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

9 Answers 9

23

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'.
...
Sign up to request clarification or add additional context in comments.

4 Comments

Your answer is great, short and effective :) you just miss $schemaReader.Dispose() which causes schema file lock
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.
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.
Why does validate take a scriptblock as input? The documentation says it needs to be of type ValidationEventHandler.
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 Comment

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.
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 Comments

The script has an error. It has no closing brace for the function.
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.
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.
Even with no arguments, or bad arguments, it doesn't do anything. PS D:\projects\svcs> .\Test-Xml.ps1 PS D:\projects\svcs>
This doesn't work; add_validationeventhandler will callback with errors, but the $failCount does not accumulate
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 Comment

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.
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

Comments

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
}

Comments

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

Comments

1

So apparently XML validation with powershell is not a hot topic, but here it is, updated for 2024, running on Powershell Core 7.4.5. ...With some security and Quality of Life Improvements.

function Test-Xml {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$XmlPath,

        [Parameter(Mandatory = $true, Position = 1)]
        [string]$XsdPath
    )
    # Normalize the path
    $XmlPath = (Resolve-Path -LiteralPath $XmlPath).ProviderPath
    $XsdPath = (Resolve-Path -LiteralPath $XsdPath).ProviderPath

    # Check if the XML and XSD files exist
    if (-not (Test-Path $XmlPath)) {
        Throw "XML file '$XmlPath' does not exist."
    }
    if (-not (Test-Path $XsdPath)) {
        Throw "XSD file '$XsdPath' does not exist."
    }

    # Read the XSD schema
    try {
        $schemaReader = [System.Xml.XmlReader]::Create($XsdPath)
        $schema = [System.Xml.Schema.XmlSchema]::Read($schemaReader, $null)
        $schemaReader.Close()
    }
    catch {
        Throw "Failed to read XSD schema from '$XsdPath': $_"
    }

    # Create an XmlSchemaSet and add the schema
    $schemaSet = New-Object System.Xml.Schema.XmlSchemaSet
    try {
        $schemaSet.XmlResolver = $null  # Disable external resource access
        $schemaSet.Add($schema) | Out-Null
        $schemaSet.Compile()
    }
    catch {
        Throw "Failed to load or compile XSD schema from '$XsdPath': $_"
    }

    # Prepare to collect validation errors
    $validationIssues = New-Object System.Collections.ArrayList
    $eventHandler = {
        param ($sender, $eventArgs)
        $validationIssues.Add($eventArgs) | Out-Null
    }

    # Set up XmlReaderSettings with appropriate validation flags
    $settings = New-Object System.Xml.XmlReaderSettings
    $settings.ValidationType = [System.Xml.ValidationType]::Schema
    $settings.Schemas = $schemaSet
    $settings.ValidationFlags = `
        [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessSchemaLocation `
        -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessInlineSchema `
        -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ReportValidationWarnings `
        -bor [System.Xml.Schema.XmlSchemaValidationFlags]::ProcessIdentityConstraints
    $settings.DtdProcessing = [System.Xml.DtdProcessing]::Prohibit
    $settings.XmlResolver = $null  # Disable external resource access

    # Attach the event handler
    $settings.add_ValidationEventHandler($eventHandler)

    # Create an XmlReader for the XML document
    try {
        $xmlReader = [System.Xml.XmlReader]::Create($XmlPath, $settings)
    }
    catch {
        Throw "Failed to read XML file '$XmlPath': $_"
    }

    # Parse and validate the XML document
    try {
        while ($xmlReader.Read()) {
            # Reading the XML triggers validation
        }
    }
    catch [System.Xml.XmlException] {
        # Catch XML parsing errors (e.g., malformed XML)
        Write-Verbose "XML parsing failed: $_"
        return $false
    }
    finally {
        $xmlReader.Close()
    }

    # Separate errors and warnings
    $warnings = $validationIssues | Where-Object { $_.Severity -eq 'Warning' }
    $errors = $validationIssues | Where-Object { $_.Severity -eq 'Error' }

    # Determine validity based on the presence of errors
    if ($errors.Count -gt 0) {
        # There are validation errors
        Write-Verbose "XML validation failed with the following errors:"
        foreach ($error in $errors) {
            Write-Verbose "Error: Line $($error.Exception.LineNumber), Position $($error.Exception.LinePosition): $($error.Message)"
        }
        if ($warnings.Count -gt 0) {
            Write-Verbose "XML validation also produced warnings:"
            foreach ($warning in $warnings) {
                Write-Verbose "Warning: Line $($warning.Exception.LineNumber), Position $($warning.Exception.LinePosition): $($warning.Message)"
            }
        }
        return [PSCustomObject]@{
            IsValid = $false
            Errors = $errors
            Warnings = $warnings
        }
    } else {
        # No validation errors; check for warnings
        if ($warnings.Count -gt 0) {
            # Only warnings present
            Write-Verbose "XML validation succeeded with warnings:"
            foreach ($warning in $warnings) {
                Write-Verbose "Warning: Line $($warning.Exception.LineNumber), Position $($warning.Exception.LinePosition): $($warning.Message)"
            }
        } else {
            # No errors or warnings
            Write-Verbose "XML validation succeeded with no errors or warnings."
        }
        return [PSCustomObject]@{
            IsValid = $true
            Errors = $errors
            Warnings = $warnings
        }
    }
}

$xmlFile = ".\WIMs\unattend.xml"
$xsdFile = ".\unattend.xsd"

Test-Xml -XmlPath $xmlFile -XsdPath $xsdFile -Verbose

returns:

VERBOSE: XML validation failed with the following errors:
VERBOSE: Error: Line 3, Position 13: The 'pass' attribute is invalid - The value 'oobeSystems' is invalid according to its datatype 'urn:schemas-microsoft-com:unattend:passType' - The Enumeration constraint failed.
VERBOSE: XML validation also produced warnings:
VERBOSE: Warning: Line 5, Position 8: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:UserAccounts'.
VERBOSE: Warning: Line 6, Position 10: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:LocalAccounts'.
VERBOSE: Warning: Line 7, Position 12: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:LocalAccount'.
VERBOSE: Warning: Line 7, Position 25: Could not find schema information for the attribute 'http://schemas.microsoft.com/WMIConfig/2002/State:action'.
VERBOSE: Warning: Line 8, Position 14: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:Name'.
VERBOSE: Warning: Line 9, Position 14: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:Password'.
VERBOSE: Warning: Line 10, Position 16: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:Value'.
VERBOSE: Warning: Line 11, Position 16: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:PlainText'.
VERBOSE: Warning: Line 13, Position 14: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:DisplayName'.
VERBOSE: Warning: Line 14, Position 14: Could not find schema information for the element 'urn:schemas-microsoft-com:unattend:Group'.

IsValid Errors                                Warnings
------- ------                                --------
  False System.Xml.Schema.ValidationEventArgs {System.Xml.Schema.ValidationEventArgs, System.Xml.S…

Comments

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.