core/ConvertTo-ObjectSortedProperties.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
Recursively sorts object properties alphabetically for deterministic JSON output.

.DESCRIPTION
Walks nested objects, dictionaries, and arrays and returns an equivalent structure with
object keys sorted alphabetically. Intended to normalize Graph API responses before
serialization so backups are stable across runs.

.PARAMETER InputObject
The input object to normalize.
#>

function ConvertTo-ObjectSortedPropertiesInternal {
    param(
        $Value
    )

    if ($null -eq $Value) {
        return $null
    }

    if (
        $Value.GetType().IsPrimitive -or
        $Value -is [string] -or
        $Value -is [decimal] -or
        $Value -is [datetime] -or
        $Value -is [datetimeoffset] -or
        $Value -is [timespan] -or
        $Value -is [guid] -or
        $Value -is [enum]
    ) {
        return $Value
    }

    if ($Value -is [System.Collections.IDictionary]) {
        $sorted = [ordered]@{}
        foreach ($entry in ($Value.GetEnumerator() | Sort-Object { [string]$_.Key })) {
            $sorted[$entry.Key] = ConvertTo-ObjectSortedPropertiesInternal -Value $entry.Value
        }
        return $sorted
    }

    if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) {
        $items = [System.Collections.Generic.List[object]]::new()
        foreach ($item in $Value) {
            $items.Add((ConvertTo-ObjectSortedPropertiesInternal -Value $item))
        }
        return , $items.ToArray()
    }

    $properties = @($Value.PSObject.Properties | Where-Object { $_.MemberType -eq [System.Management.Automation.PSMemberTypes]::NoteProperty })
    if ($properties.Count -gt 0) {
        $sortedObject = [ordered]@{}
        foreach ($property in ($properties | Sort-Object Name)) {
            $sortedObject[$property.Name] = ConvertTo-ObjectSortedPropertiesInternal -Value $property.Value
        }
        return [PSCustomObject]$sortedObject
    }

    return $Value
}

function ConvertTo-ObjectSortedProperties {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [AllowNull()]
        $InputObject
    )

    process {
        return (ConvertTo-ObjectSortedPropertiesInternal -Value $InputObject)
    }
}

Export-ModuleMember -Function ConvertTo-ObjectSortedProperties