Public/Convert-XMLtoPSObject.ps1

Function Convert-XMLtoPSObject {

    <#
    .SYNOPSIS
        Converts an [xml] object into a nested PSCustomObject, so it can easily be turned into JSON.
 
    .DESCRIPTION
        Apteco PS Modules - Convert XML to PSCustomObject
 
        Recursively walks an xml tree and turns tags into NoteProperties and attributes into NoteProperties
        prefixed with -AttributesPrefix (default "@"). A tag name used multiple times at the same level is
        collected into an array.
 
        Inspired by the C# example from https://dev.to/adamkdean/xml-to-hashtable-59dg
 
        Example call:
 
        $xmlInputString = @"
        <?xml version="1.0" encoding="utf-8"?>
        <XmlSerialisationWrapper xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xml:space="preserve">
            <CurrentDate>20201130</CurrentDate>
            <Location>Germany</Location>
            <Messages errors="0" warnings="0" info="1">All fine!</Messages>
            <Obj xsi:type="TablePersistentState">
                <Book id="123">
                    <Title>Book Nr 1</Title>
                    <Author>Author Nr 1</Author>
                    <Recommendations positive="10" neutral="5" negative="1"/>
                </Book>
                <Book id="456">
                    <Title>Book Nr 2</Title>
                    <Author>Author Nr 2</Author>
                    <Recommendations positive="0" neutral="5" negative="10"/>
                </Book>
            </Obj>
        </XmlSerialisationWrapper>
        "@
 
        $xmlObj = [xml]$xmlInputString
        $pscustom = $xmlObj | Convert-XMLtoPSObject
        $json = $pscustom | ConvertTo-Json -Depth 20
 
        # ... results in a json like this one
        {
            "xml": "version=\"1.0\" encoding=\"utf-8\"",
            "XmlSerialisationWrapper": {
                "CurrentDate": "20201130",
                "Location": "Germany",
                "Messages": {
                    "@errors": "0",
                    "@warnings": "0",
                    "@info": "1",
                    "value": "All fine!"
                },
                "Obj": {
                    "@type": "TablePersistentState",
                    "Book": [
                        {
                            "@id": "123",
                            "Title": "Book Nr 1",
                            "Author": "Author Nr 1",
                            "Recommendations": { "@positive": "10", "@neutral": "5", "@negative": "1" }
                        },
                        {
                            "@id": "456",
                            "Title": "Book Nr 2",
                            "Author": "Author Nr 2",
                            "Recommendations": { "@positive": "0", "@neutral": "5", "@negative": "10" }
                        }
                    ]
                }
            }
        }
 
    .PARAMETER XML
        The xml node to convert. Load it first as [xml]$xmlString, then pipe it in or pass -XML.
 
    .PARAMETER AttributesPrefix
        Prefix used for the NoteProperty created from an xml attribute. Default is "@".
 
    .EXAMPLE
        [xml]$xmlObj = Get-Content "file.xml"
        $xmlObj | Convert-XMLtoPSObject | ConvertTo-Json -Depth 20
 
    .EXAMPLE
        Convert-XMLtoPSObject -XML ([xml]"<root><a>1</a><a>2</a></root>")
 
    .INPUTS
        System.Xml.XmlNode
 
    .OUTPUTS
        PSCustomObject
 
    .NOTES
        Author: florian.von.bracht@apteco.de
        Rescued from a 2020-11-30 local-only script
 
    #>


    [CmdletBinding()]
    Param (
         [Parameter(Mandatory=$true, ValueFromPipeline=$true)] $XML
        ,[Parameter(Mandatory=$false)][String]$AttributesPrefix = "@"
    )

    Begin {

        # xml attributes that are always excluded (namespace/schema noise, not real data)
        $excludeAttributes = @("xsd", "xsi", "space", "xmlns", "nil")

    }

    Process {

        $return = New-Object -TypeName PSCustomObject

        # Handle attributes of the xml tag itself
        If ( $XML.Attributes.Count -gt 0 ) {
            $XML.Attributes | ForEach-Object {
                $a = $_
                $attName = $a.LocalName
                $attValue = $a.'#text'
                If ( $attName -notin $excludeAttributes ) {
                    $return | Add-Member -MemberType NoteProperty -Name "$( $AttributesPrefix )$( $attName )" -Value $attValue
                }
            }
        }

        # Loop through all child nodes and save them to the object, or recurse into them
        $XML.ChildNodes | Where-Object { $_.NodeType -notin @([System.Xml.XmlNodeType]::SignificantWhitespace) } | ForEach-Object {

            $n = $_
            $name = $n.Get_name() # Using Get_name() rather than .Name, since an attribute literally named "name" would shadow .Name

            # Decide if we go recursively or use the current value
            If ( $n.HasChildNodes ) {
                If ( $n.ChildNodes.Count -gt 1 ) {
                    $value = [PSCustomObject]( Convert-XMLtoPSObject -XML $n -AttributesPrefix $AttributesPrefix )
                } Else {
                    If ( $n.ChildNodes[0].NodeType -eq [System.Xml.XmlNodeType]::Text ) {
                        # Handle attributes on a tag that only has a single text child
                        If ( $n.Attributes.Count -gt 0 ) {
                            $value = New-Object -TypeName PSCustomObject
                            $n.Attributes | ForEach-Object {
                                $a = $_
                                $attName = $a.LocalName
                                $attValue = $a.'#text'
                                If ( $attName -notin $excludeAttributes ) {
                                    $value | Add-Member -MemberType NoteProperty -Name "$( $AttributesPrefix )$( $attName )" -Value $attValue
                                }
                            }
                            $v = $n.ChildNodes[0].Value
                            If ( $null -ne $v ) {
                                $value | Add-Member -MemberType NoteProperty -Name "value" -Value $v
                            }
                        } Else {
                            $value = $n.ChildNodes[0].Value
                        }
                    } Else {
                        $value = [PSCustomObject]( Convert-XMLtoPSObject -XML $n -AttributesPrefix $AttributesPrefix )
                    }
                }
            } Else {
                # Handle attributes on a childless tag
                If ( $n.Attributes.Count -gt 0 ) {
                    $value = New-Object -TypeName PSCustomObject
                    $n.Attributes | ForEach-Object {
                        $a = $_
                        $attName = $a.LocalName
                        $attValue = $a.'#text'
                        If ( $attName -notin $excludeAttributes ) {
                            $value | Add-Member -MemberType NoteProperty -Name "$( $AttributesPrefix )$( $attName )" -Value $attValue
                        }
                    }
                    $v = $n.ChildNodes[0].Value
                    If ( $null -ne $v ) {
                        $value | Add-Member -MemberType NoteProperty -Name "value" -Value $v
                    }
                } Else {
                    $value = $n.Value
                }
            }

            # A tag name can occur multiple times at the same level -- collect those into an array
            If ( ( $return | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -eq $name } ).Count -ge 1 ) {

                If ( $return.$name -is [System.Collections.ArrayList] ) {
                    # An array already exists, add to it
                    $list = $return.$name
                    $list.Add($value) | Out-Null # Out-Null is important here -- without it, .Add()'s return value leaks into the recursive function's output
                    $return.$name = $list
                } Else {
                    # A single value exists (scalar or PSCustomObject), turn it into an array
                    $list = [System.Collections.ArrayList]@()
                    $list.Add($return.$name) | Out-Null
                    $list.Add($value) | Out-Null
                    $return.$name = $list
                }

            } Else {
                $return | Add-Member -MemberType NoteProperty -Name $name -Value $value
            }

        }

        return $return

    }

}