Question

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.

Was it helpful?

Solution 2

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"
}
}

OTHER TIPS

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.

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

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
    })

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

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

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
}

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top