36

As a part of my development I'd like to be able to validate an entire folder's worth of XML files against a single XSD file. A PowerShell function seems like a good candidate for this as I can then just pipe a list of files to it like so: dir *.xml | Validate-Xml -Schema .\MySchema.xsd

I've considered porting C# code from the Validating an Xml against Referenced XSD in C# question, but I don't know how to Add handlers in PowerShell.

2
  • Why exactly do you need it be PowerShell just because you're reading a list of files from stdin? Commented May 5, 2009 at 2:25
  • 3
    I'd like to be able to easily integrate it into automated build scripts. Didn't want to have to compile an app just to do this. A PowerShell script seemed like a natural fit for this kind of thing. Commented May 5, 2009 at 3:33

8 Answers 8

22

I want to comment that the script in current accepted answer doesn't validate errors about incorrect orders of elements of xs:sequence. For example: test.xml

<addresses xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:noNamespaceSchemaLocation='test.xsd'>
  <address>
    <street>Baker street 5</street>
    <name>Joe Tester</name>
  </address>
</addresses>

test.xsd

<xs:schema xmlns:xs='http://www.w3.org/2001/XMLSchema'>    
<xs:element name="addresses">
      <xs:complexType>
       <xs:sequence>
         <xs:element ref="address" minOccurs='1' maxOccurs='unbounded'/>
       </xs:sequence>
     </xs:complexType>
    </xs:element>

     <xs:element name="address">
      <xs:complexType>
       <xs:sequence>
         <xs:element ref="name" minOccurs='0' maxOccurs='1'/>
         <xs:element ref="street" minOccurs='0' maxOccurs='1'/>
       </xs:sequence>
      </xs:complexType>
     </xs:element>

     <xs:element name="name" type='xs:string'/>
     <xs:element name="street" type='xs:string'/>
    </xs:schema>

I wrote another version that can report this error:

function Test-XmlFile
{
    <#
    .Synopsis
        Validates an xml file against an xml schema file.
    .Example
        PS> dir *.xml | Test-XmlFile schema.xsd
    #>
    [CmdletBinding()]
    param (     
        [Parameter(Mandatory=$true)]
        [string] $SchemaFile,

        [Parameter(ValueFromPipeline=$true, Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [alias('Fullname')]
        [string] $XmlFile,

        [scriptblock] $ValidationEventHandler = { Write-Error $args[1].Exception }
    )

    begin {
        $schemaReader = New-Object System.Xml.XmlTextReader $SchemaFile
        $schema = [System.Xml.Schema.XmlSchema]::Read($schemaReader, $ValidationEventHandler)
    }

    process {
        $ret = $true
        try {
            $xml = New-Object System.Xml.XmlDocument
            $xml.Schemas.Add($schema) | Out-Null
            $xml.Load($XmlFile)
            $xml.Validate({
                    throw ([PsCustomObject] @{
                        SchemaFile = $SchemaFile
                        XmlFile = $XmlFile
                        Exception = $args[1].Exception
                    })
                })
        } catch {
            Write-Error $_
            $ret = $false
        }
        $ret
    }

    end {
        $schemaReader.Close()
    }
}

PS C:\temp\lab-xml-validation> dir test.xml | Test-XmlFile test.xsd

System.Xml.Schema.XmlSchemaValidationException: The element 'address' has invalid child element 'name'.
...
4
  • 2
    Your answer is great, short and effective :) you just miss $schemaReader.Dispose() which causes schema file lock
    – Adassko
    Commented Jan 9, 2018 at 14:08
  • 1
    Thanks; I have updated my answer with an updated version that is written as a function that can be included in a module and supports pipeline.
    – wangzq
    Commented Jan 9, 2018 at 22:41
  • 1
    For others viewing this, it does work on PSv4 (running on a Windows Server 2012 R2 box). And by extension, this will work in PSv5 and PSv5.1, too. Not sure about PS Core, I haven't tried. Commented Nov 11, 2020 at 20:48
  • Why does validate take a scriptblock as input? The documentation says it needs to be of type ValidationEventHandler.
    – Blaisem
    Commented Dec 24, 2022 at 20:33
17

The PowerShell Community Extensions has a Test-Xml cmdlet. The only downside is the extensions havn't been updated for awhile, but most do work on the lastest version of powershell (including Test-Xml). Just do a Get-Childitem's and pass the list to a foreach, calling Test-Xml on each.

1
  • 1
    v1.2 of the extensions were released to support v2 of PowerShell. They all seem to work well so I'm unsure of any downsides.
    – Scott Saad
    Commented Jan 21, 2010 at 3:25
13

I wrote a PowerShell function to do this:

Usage:

dir *.xml | Test-Xml -Schema ".\MySchemaFile.xsd" -Namespace "http://tempuri.org"

Code:

function Test-Xml {
    param(
        $InputObject = $null,
        $Namespace = $null,
        $SchemaFile = $null
    )

    BEGIN {
        $failCount = 0
        $failureMessages = ""
        $fileName = ""
    }

    PROCESS {
        if ($InputObject -and $_) {
            throw 'ParameterBinderStrings\AmbiguousParameterSet'
            break
        } elseif ($InputObject) {
            $InputObject
        } elseif ($_) {
            $fileName = $_.FullName
            $readerSettings = New-Object -TypeName System.Xml.XmlReaderSettings
            $readerSettings.ValidationType = [System.Xml.ValidationType]::Schema
            $readerSettings.ValidationFlags = [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessInlineSchema -bor
                [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessSchemaLocation -bor 
                [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings
            $readerSettings.Schemas.Add($Namespace, $SchemaFile) | Out-Null
            $readerSettings.add_ValidationEventHandler(
            {
                $failureMessages = $failureMessages + [System.Environment]::NewLine + $fileName + " - " + $_.Message
                $failCount = $failCount + 1
            });
            $reader = [System.Xml.XmlReader]::Create($_, $readerSettings)
            while ($reader.Read()) { }
            $reader.Close()
        } else {
            throw 'ParameterBinderStrings\InputObjectNotBound'
        }
    }

    END {
        $failureMessages
        "$failCount validation errors were found"
    }
}
5
  • The script has an error. It has no closing brace for the function. Commented Jan 18, 2010 at 8:08
  • The $reader should be closed after the while-loop. Otherwise you won't be able to edit the file until the Finalizer-saftey-net kicks in. Commented Sep 1, 2010 at 11:11
  • This doesn't seem to work: PS D:\projects\svcs> dir *.xml | Test-Xml The term 'Test-Xml' is not recognized as the name of a cmdlet, function, script file, or operable program.
    – Chloe
    Commented Jul 6, 2012 at 15:52
  • Even with no arguments, or bad arguments, it doesn't do anything. PS D:\projects\svcs> .\Test-Xml.ps1 PS D:\projects\svcs>
    – Chloe
    Commented Jul 6, 2012 at 15:53
  • This doesn't work; add_validationeventhandler will callback with errors, but the $failCount does not accumulate
    – eoleary
    Commented Oct 7, 2022 at 18:00
4

I am using this simple snippet, always works and you don't need complicated functions. It this example I am loading configuration xml with data which are used later for deployment and server configuration:

# You probably don't need this, it's just my way
$script:Context = New-Object -TypeName System.Management.Automation.PSObject
Add-Member -InputObject $Context -MemberType NoteProperty -Name Configuration -Value ""
$ConfigurationPath = $(Join-Path -Path $PWD -ChildPath "Configuration")

# Load xml and its schema
$Context.Configuration = [xml](Get-Content -LiteralPath $(Join-Path -Path $ConfigurationPath -ChildPath "Configuration.xml"))
$Context.Configuration.Schemas.Add($null, $(Join-Path -Path $ConfigurationPath -ChildPath "Configuration.xsd")) | Out-Null

# Validate xml against schema
$Context.Configuration.Validate(
    {
        Write-Host "ERROR: The Configuration-File Configuration.xml is not valid. $($_.Message)" -ForegroundColor Red

        exit 1
    })
1
  • 1
    This is the simplest (and therefore usually best) solution. The only problem is that it doesn't work for a schema that has a target namespace other than the empty string. To handle this case, you must load the XmlSchema object separately.
    – ssamuel
    Commented May 28, 2013 at 23:45
3

the solution of (Flatliner DOA) is working good on PSv2, but not on Server 2012 PSv3.

the solution of (wangzq) is working on PS2 and PS3!!

anyone who needs an xml validation on PS3, can use this (based on wangzq's function)

function Test-Xml {
    param (
    [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [string] $XmlFile,

        [Parameter(Mandatory=$true)]
        [string] $SchemaFile
    )

    [string[]]$Script:XmlValidationErrorLog = @()
    [scriptblock] $ValidationEventHandler = {
        $Script:XmlValidationErrorLog += $args[1].Exception.Message
    }

    $xml = New-Object System.Xml.XmlDocument
    $schemaReader = New-Object System.Xml.XmlTextReader $SchemaFile
    $schema = [System.Xml.Schema.XmlSchema]::Read($schemaReader, $ValidationEventHandler)
    $xml.Schemas.Add($schema) | Out-Null
    $xml.Load($XmlFile)
    $xml.Validate($ValidationEventHandler)

    if ($Script:XmlValidationErrorLog) {
        Write-Warning "$($Script:XmlValidationErrorLog.Count) errors found"
        Write-Error "$Script:XmlValidationErrorLog"
    }
    else {
        Write-Host "The script is valid"
    }
}

Test-Xml -XmlFile $XmlFile -SchemaFile $SchemaFile
2

I realise this is an old question however I tried the answers provided and could not get them to work successfully in Powershell.

I have created the following function which uses some of the techniques described here. I have found it very reliable.

I had to validate XML documents before at various times however I always found the line number to be 0. It appears the XmlSchemaException.LineNumber will only be available while loading the document.

If you do validation afterwards using the Validate() method on an XmlDocument then LineNumber/LinePosition will always be 0.

Instead you should do validation while reading using an XmlReader and adding a validation event handler to a block of script.

Function Test-Xml()
{
    [CmdletBinding(PositionalBinding=$false)]
    param (
    [Parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [string] [ValidateScript({Test-Path -Path $_})] $Path,

        [Parameter(Mandatory=$true)]
        [string] [ValidateScript({Test-Path -Path $_})] $SchemaFilePath,

        [Parameter(Mandatory=$false)]
        $Namespace = $null
    )

    [string[]]$Script:XmlValidationErrorLog = @()
    [scriptblock] $ValidationEventHandler = {
        $Script:XmlValidationErrorLog += "`n" + "Line: $($_.Exception.LineNumber) Offset: $($_.Exception.LinePosition) - $($_.Message)"
    }

    $readerSettings = New-Object -TypeName System.Xml.XmlReaderSettings
    $readerSettings.ValidationType = [System.Xml.ValidationType]::Schema
    $readerSettings.ValidationFlags = [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints -bor
            [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessSchemaLocation -bor 
            [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings
    $readerSettings.Schemas.Add($Namespace, $SchemaFilePath) | Out-Null
    $readerSettings.add_ValidationEventHandler($ValidationEventHandler)
    try 
    {
        $reader = [System.Xml.XmlReader]::Create($Path, $readerSettings)
        while ($reader.Read()) { }
    }

    #handler to ensure we always close the reader sicne it locks files
    finally 
    {
        $reader.Close()
    }

    if ($Script:XmlValidationErrorLog) 
    {
        [string[]]$ValidationErrors = $Script:XmlValidationErrorLog
        Write-Warning "Xml file ""$Path"" is NOT valid according to schema ""$SchemaFilePath"""
        Write-Warning "$($Script:XmlValidationErrorLog.Count) errors found"
    }
    else 
    {
        Write-Host "Xml file ""$Path"" is valid according to schema ""$SchemaFilePath"""
    }

    Return ,$ValidationErrors #The comma prevents powershell from unravelling the collection http://bit.ly/1fcZovr
}
1

I have created a separate PowerShell file which can perform XSD validation on XML files with an inline schema reference. Works really well. Download and howto are available on https://knowledge.zomers.eu/PowerShell/Pages/How-to-validate-XML-against-an-XSD-schema-using-PowerShell.aspx

0
0

I re-wrote it (I know bad habbit) , but the starting script by @Flatliner_DOA was too good to discard completely.

function Test-Xml {
[cmdletbinding()]
param(
    [parameter(mandatory=$true)]$InputFile,
    $Namespace = $null,
    [parameter(mandatory=$true)]$SchemaFile
)

BEGIN {
    $failCount = 0
    $failureMessages = ""
    $fileName = ""
}

PROCESS {
    if ($inputfile)
    {
        write-verbose "input file: $inputfile"
        write-verbose "schemafile: $SchemaFile"
        $fileName = (resolve-path $inputfile).path
        if (-not (test-path $SchemaFile)) {throw "schemafile not found $schemafile"}
        $readerSettings = New-Object -TypeName System.Xml.XmlReaderSettings
        $readerSettings.ValidationType = [System.Xml.ValidationType]::Schema
        $readerSettings.ValidationFlags = [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints -bor
            [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessSchemaLocation -bor 
            [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings
        $readerSettings.Schemas.Add($Namespace, $SchemaFile) | Out-Null
        $readerSettings.add_ValidationEventHandler(
        {
            try {
                $detail = $_.Message 
                $detail += "`n" + "On Line: $($_.exception.linenumber) Offset: $($_.exception.lineposition)"
            } catch {}
            $failureMessages += $detail
            $failCount = $failCount + 1
        });
        try {
            $reader = [System.Xml.XmlReader]::Create($fileName, $readerSettings)
            while ($reader.Read()) { }
        }
        #handler to ensure we always close the reader sicne it locks files
        finally {
            $reader.Close()
        }
    } else {
        throw 'no input file'
    }
}

END {
    if ($failureMessages)
    { $failureMessages}
    write-verbose "$failCount validation errors were found"

}
}

#example calling/useage  code follows:
$erroractionpreference = 'stop'
Set-strictmode -version 2

$valid = @(Test-Xml -inputfile $inputfile -schemafile $XSDPath )
write-host "Found ($($valid.count)) errors"
if ($valid.count) {
    $valid |write-host -foregroundcolor red
}

The function no longer pipelines as an alternative to using a file-path, it's a complication this use-case does not need. Feel free to hack the begin/process/end handlers away.

Not the answer you're looking for? Browse other questions tagged or ask your own question.