Private/Generator/ConvertTo-OpenApiGenJson.ps1

function ConvertTo-OpenApiGenJson {
    <#
    .SYNOPSIS
        Serialises an object to JSON text that is identical on every machine and PowerShell edition.

    .DESCRIPTION
        ConvertTo-Json formats differently on Windows PowerShell 5.1 and PowerShell 7, so the generator
        uses this writer instead: two-space indentation, LF line endings, invariant number formats,
        non-ASCII characters escaped as \uXXXX (the output is plain ASCII), ordered dictionaries and
        PSCustomObjects in their own order and other dictionaries sorted by key (ordinal). A reference
        cycle is written as { "RefName": ..., "Recursive": true } (or null) instead of recursing.
        -Compress writes everything on one line without spaces. -SkipEmpty leaves out map entries whose
        value is null, an empty string, an empty array or an empty map.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter()]
        [AllowNull()]
        [object]$InputObject,

        [Parameter()]
        [switch]$Compress,

        [Parameter()]
        [switch]$SkipEmpty
    )

    $builder = New-Object -TypeName System.Text.StringBuilder
    $newLine = "`n"
    $indentUnit = ' '
    $colon = ': '
    if ($Compress) {
        $newLine = ''
        $indentUnit = ''
        $colon = ':'
    }
    $isEmpty = {
        param($Item)
        if ($null -eq $Item) {
            return $true
        }
        if ($Item -is [string]) {
            return $Item.Length -eq 0
        }
        if ($Item -is [System.Collections.IDictionary]) {
            return $Item.Count -eq 0
        }
        if ($Item -is [System.Management.Automation.PSCustomObject]) {
            return @($Item.PSObject.Properties).Count -eq 0
        }
        if ($Item -is [System.Collections.ICollection]) {
            return $Item.Count -eq 0
        }
        return $false
    }
    $ancestors = New-Object -TypeName System.Collections.ArrayList
    $escapeEvaluator = [System.Text.RegularExpressions.MatchEvaluator] {
        param($Match)

        switch ([int][char]$Match.Value) {
            8 { return '\b' }
            9 { return '\t' }
            10 { return '\n' }
            12 { return '\f' }
            13 { return '\r' }
            34 { return '\"' }
            92 { return '\\' }
        }
        return '\u' + ([int][char]$Match.Value).ToString('x4', [System.Globalization.CultureInfo]::InvariantCulture)
    }
    $writer = {
        param($Value, [int]$Depth)

        $indent = $newLine + ($indentUnit * ($Depth + 1))
        $closeIndent = $newLine + ($indentUnit * $Depth)
        if ($null -eq $Value) {
            [void]$builder.Append('null')
            return
        }
        if ($Value -is [System.Management.Automation.SwitchParameter]) {
            $Value = $Value.IsPresent
        }
        if ($Value -is [bool]) {
            [void]$builder.Append($(if ($Value) { 'true' } else { 'false' }))
            return
        }
        if ($Value -is [string] -or $Value -is [char] -or $Value -is [guid] -or $Value -is [uri] -or $Value -is [enum] -or $Value -is [version]) {
            $escaped = [regex]::Replace([string]$Value, '["\\\u0000-\u001F\u007F-\uFFFF]', $escapeEvaluator)
            [void]$builder.Append('"' + $escaped + '"')
            return
        }
        if ($Value -is [datetime]) {
            [void]$builder.Append('"' + $Value.ToString('o', [System.Globalization.CultureInfo]::InvariantCulture) + '"')
            return
        }
        if ($Value -is [double] -or $Value -is [single]) {
            if ([double]::IsNaN($Value) -or [double]::IsInfinity($Value)) {
                [void]$builder.Append('null')
            }
            else {
                [void]$builder.Append(([double]$Value).ToString('R', [System.Globalization.CultureInfo]::InvariantCulture))
            }
            return
        }
        if ($Value -is [byte] -or $Value -is [sbyte] -or $Value -is [int16] -or $Value -is [uint16] -or $Value -is [int] -or $Value -is [uint32] -or $Value -is [long] -or $Value -is [uint64] -or $Value -is [decimal] -or $Value.GetType().FullName -eq 'System.Numerics.BigInteger') {
            [void]$builder.Append(([System.IFormattable]$Value).ToString($null, [System.Globalization.CultureInfo]::InvariantCulture))
            return
        }

        # Containers: stop at reference cycles
        foreach ($ancestor in $ancestors) {
            if ([object]::ReferenceEquals($ancestor, $Value)) {
                $refName = $null
                if (-not ($Value -is [System.Collections.IEnumerable]) -or $Value -is [System.Collections.IDictionary]) {
                    $refName = Get-OpenApiGenMapValue -Map $Value -Key 'RefName'
                }
                if ($null -eq $refName) {
                    [void]$builder.Append('null')
                }
                else {
                    & $writer ([ordered]@{ RefName = $refName; Recursive = $true }) $Depth
                }
                return
            }
        }
        [void]$ancestors.Add($Value)
        try {
            $isMap = ($Value -is [System.Collections.IDictionary]) -or ($Value -is [System.Management.Automation.PSCustomObject])
            if ($isMap) {
                $entries = @(Get-OpenApiGenMapEntry -Map $Value)
                if ($SkipEmpty) {
                    $entries = @($entries | Where-Object -FilterScript { -not (& $isEmpty $_.Value) })
                }
                if ($entries.Count -eq 0) {
                    [void]$builder.Append('{}')
                    return
                }
                [void]$builder.Append('{')
                for ($i = 0; $i -lt $entries.Count; $i++) {
                    [void]$builder.Append($indent)
                    & $writer ([string]$entries[$i].Key) 0
                    [void]$builder.Append($colon)
                    & $writer $entries[$i].Value ($Depth + 1)
                    if ($i -lt $entries.Count - 1) {
                        [void]$builder.Append(',')
                    }
                }
                [void]$builder.Append($closeIndent + '}')
                return
            }
            if ($Value -is [System.Collections.IEnumerable]) {
                $items = @(foreach ($item in $Value) { , $item })
                if ($items.Count -eq 0) {
                    [void]$builder.Append('[]')
                    return
                }
                [void]$builder.Append('[')
                for ($i = 0; $i -lt $items.Count; $i++) {
                    [void]$builder.Append($indent)
                    & $writer $items[$i] ($Depth + 1)
                    if ($i -lt $items.Count - 1) {
                        [void]$builder.Append(',')
                    }
                }
                [void]$builder.Append($closeIndent + ']')
                return
            }
            # Any other object: its public properties, in declaration order
            $objectMap = [ordered]@{}
            foreach ($property in $Value.PSObject.Properties) {
                if ($property.MemberType -match 'Property') {
                    $objectMap[$property.Name] = $property.Value
                }
            }
            & $writer $objectMap $Depth
        }
        finally {
            [void]$ancestors.RemoveAt($ancestors.Count - 1)
        }
    }

    & $writer $InputObject 0
    return $builder.ToString()
}