Private/Helper/ConvertTo-CWMRequestBody.ps1

function ConvertTo-CWMRequestBody {
    # Shared by Invoke-CWMNewMaster and Invoke-CWMSetMaster. Returns $Arguments.Body unchanged when given
    # (a string or a byte[] multipart body); otherwise the remaining arguments as JSON, leaving out
    # PowerShell common parameters, module-only parameters and the names in $Skip.
    [CmdletBinding()]
    param(
        $Arguments,
        [string[]]$Skip
    )

    # The unary comma stops a byte[] body from being written to the pipeline one byte at a time
    if ($Arguments.Body) { return , $Arguments.Body }

    # 'Version' is no longer used (the API version comes from Connect-CWM) but is still kept out of the
    # body, so older scripts that pass it in -Arguments do not send it to the API.
    $Skip += 'Debug', 'ErrorAction', 'ErrorVariable', 'InformationAction', 'InformationVariable', 'OutVariable', 'OutBuffer', 'PipelineVariable', 'ProgressAction', 'Verbose', 'WarningAction', 'WarningVariable', 'WhatIf', 'Confirm', 'Version', 'grandparentId', 'parentId'

    $Body = @{}
    foreach ($i in $Arguments.GetEnumerator()) {
        if ($Skip -notcontains $i.Key) {
            $Value = $i.value
            # Values without dates are added untouched, exactly as before
            if (Test-CWMBodyDate $Value) {
                $Value = ConvertTo-CWMBodyDate $Value
                if ($Value -is [array]) { $Value = $Value.psobject.BaseObject }
            }
            $Body.Add($i.Key, $Value)
        }
    }

    ConvertTo-Json $Body -Depth 100
}

function Test-CWMBodyDate {
    # True when $Value is, or contains (in hashtables, objects or arrays), a [datetime] or [DateTimeOffset]
    param($Value)

    if ($null -eq $Value -or $Value -is [string]) { return $false }
    if ($Value -is [datetime] -or $Value -is [DateTimeOffset]) { return $true }
    if ($Value -is [System.Collections.IDictionary]) {
        foreach ($Key in $Value.Keys) { if (Test-CWMBodyDate $Value[$Key]) { return $true } }
        return $false
    }
    if ($Value -is [System.Management.Automation.PSCustomObject]) {
        foreach ($Property in $Value.PSObject.Properties) { if (Test-CWMBodyDate $Property.Value) { return $true } }
        return $false
    }
    if ($Value -is [array]) {
        foreach ($Item in $Value) { if (Test-CWMBodyDate $Item) { return $true } }
    }
    $false
}

function ConvertTo-CWMBodyDate {
    # Returns a copy of $Value with every [datetime]/[DateTimeOffset] in it (also inside hashtables, objects
    # and arrays) replaced by an ISO 8601 UTC string. ConvertTo-Json writes dates differently per edition
    # (5.1: "\/Date(ms)\/", 7: ISO with the local offset, or no offset for unspecified-kind dates). The
    # caller's objects are never changed.
    # Dates of unspecified kind (e.g. [datetime]'2026-01-02') are sent as UTC unchanged, which is what
    # PowerShell 7 sent before (no offset, read as UTC). Converting them as local time would move a calendar
    # date to the previous day for users east of UTC. Local and UTC dates are converted to UTC.
    # Arrays returned by a function are PSObject-wrapped, which 5.1's ConvertTo-Json writes as
    # {"value":[..],"Count":n}; every converted array is therefore unwrapped before it is stored.
    param($Value)

    if ($null -eq $Value) { return $null }
    if ($Value -is [datetime]) {
        if ($Value.Kind -ne [System.DateTimeKind]::Unspecified) { $Value = $Value.ToUniversalTime() }
        return $Value.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", [cultureinfo]::InvariantCulture)
    }
    if ($Value -is [DateTimeOffset]) {
        return $Value.UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", [cultureinfo]::InvariantCulture)
    }
    if ($Value -is [System.Collections.IDictionary]) {
        $Copy = [ordered]@{}
        foreach ($Key in $Value.Keys) {
            $Converted = ConvertTo-CWMBodyDate $Value[$Key]
            if ($Converted -is [array]) { $Converted = $Converted.psobject.BaseObject }
            $Copy[$Key] = $Converted
        }
        return $Copy
    }
    if ($Value -is [System.Management.Automation.PSCustomObject]) {
        $Copy = [ordered]@{}
        foreach ($Property in $Value.PSObject.Properties) {
            $Converted = ConvertTo-CWMBodyDate $Property.Value
            if ($Converted -is [array]) { $Converted = $Converted.psobject.BaseObject }
            $Copy[$Property.Name] = $Converted
        }
        return [pscustomobject]$Copy
    }
    if ($Value -is [array]) {
        $Items = New-Object System.Collections.Generic.List[object]
        foreach ($Item in $Value) {
            $Converted = ConvertTo-CWMBodyDate $Item
            if ($Converted -is [array]) { $Converted = $Converted.psobject.BaseObject }
            $Items.Add($Converted)
        }
        # The unary comma keeps the array (also with one element) from being unrolled
        return , $Items.ToArray()
    }
    $Value
}