JSONLab.psm1


function Merge-PSObject
{
<#
.SYNOPSIS
Create a new PSObject by recursively combining the properties of PSObjects.
 
.INPUTS
System.Management.Automation.PSObject to combine.
 
.OUTPUTS
System.Management.Automation.PSObject combining the inputs.
 
.FUNCTIONALITY
PowerShell
 
.LINK
Get-Member
 
.LINK
Add-Member
 
.EXAMPLE
Merge-PSObject ([pscustomobject]@{a=1;b=2}) ([pscustomobject]@{b=0;c=3})
 
a b c
- - -
1 2 3
 
.EXAMPLE
Merge-PSObject ([pscustomobject]@{a=1;b=2}) ([pscustomobject]@{b=0;c=3}) -Force
 
a b c
- - -
1 0 3
 
.EXAMPLE
'{"a":1,"b":{"u":3},"c":{"v":5}}','{"a":{"w":8},"b":2,"c":{"x":6}}' |ConvertFrom-Json |Merge-PSObject -Accumulate -Force |select -Last 1 |ConvertTo-Json
 
{
  "a": {
    "w": 8
  },
  "b": 2,
  "c": {
    "v": 5,
    "x": 6
  }
}
#>


[CmdletBinding()][OutputType([PSObject])] Param(
# Initial PSObject to combine.
[Parameter(Position=0)][PSObject] $ReferenceObject = [pscustomobject]@{},
<#
PSObjects to combine. PSObject descendant properties are recursively merged.
Primitive values are overwritten by any matching ones in the new PSObject.
#>

[Parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true)][PSObject] $InputObject,
# Continue merging each pipeline object's properties into the same accumulator object.
[switch] $Accumulate,
# Overwrite existing properties.
[switch] $Force
)
Begin {if($Accumulate) {$value = $ReferenceObject.PSObject.Copy()}}
Process
{
    if(!$Accumulate) {$value = $ReferenceObject.PSObject.Copy()}
    foreach($p in $InputObject |Get-Member -Type Properties)
    {
        $name,$type = $p.Name,$p.MemberType
        $newvalue = $InputObject.$name
        if(!($value |Get-Member $name -Type $type))
        {
            $value |Add-Member $name -Type $type -Value $newvalue
        }
        elseif($Force)
        {
            $currentvalue = $value.$name
            $value.$name =
                if($currentvalue -isnot [PSObject] -or $newvalue -isnot [PSObject]) {$newvalue}
                else {Merge-PSObject $currentvalue $newvalue}
        }
        elseif($value.$name -is [PSObject] -and $newvalue -is [PSObject])
        {
            $value.$name = Merge-PSObject $value.$name $newvalue
        }
    }
    return $value
}

}

function Export-Json
{
<#
.SYNOPSIS
Exports a portion of a JSON document, recursively importing references.
 
.INPUTS
System.String containing JSON, or
System.Collections.Hashtable parsed from JSON, or
System.Management.Automation.PSObject parsed from JSON.
 
.OUTPUTS
System.String containing the extracted JSON.
 
.FUNCTIONALITY
Json
 
.LINK
http://jsonref.org/
 
.LINK
https://www.rfc-editor.org/rfc/rfc6901
 
.LINK
https://github.com/MicrosoftDocs/PowerShell-Docs/pull/9042
 
.LINK
Select-Json
 
.EXAMPLE
'{d:{a:{b:1,c:{"$ref":"#/d/two"}},two:2}}' |Export-Json /d/a
 
{
  "b": 1,
  "c": 2
}
 
.EXAMPLE
'{d:{a:{b:1,c:{"$ref":"#/d/c"}},c:{d:{"$ref":"#/d/two"}},two:2}}' |Export-Json /d/a -Compress
 
{"b":1,"c":{"d":2}}
#>


[CmdletBinding()][OutputType([string])] Param(
<#
The full path name of the property to get, as a JSON Pointer, modified to support wildcards:
~0 = ~ ~1 = / ~2 = ? ~3 = * ~4 = [
#>

[Parameter(Position=0)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~[0-4])*)\z')]
[string] $JsonPointer = '',
# The JSON (string or parsed object/hashtable) to get the value from.
[Parameter(ParameterSetName='InputObject',ValueFromPipeline=$true)] $InputObject,
# A JSON file to update.
[Parameter(ParameterSetName='Path',Mandatory=$true)][string] $Path,
# Omits white space and indented formatting in the output string.
[switch] $Compress
)
Begin
{
    if($PSVersionTable.PSVersion -lt 7.3) {Write-Warning "JSON property order may be changed in PowerShell < 7.3"}

    function Get-Reference
    {
        [CmdletBinding()] Param([uri] $ReferenceUri, $Root)
        $source,$pointer = switch($ReferenceUri)
        {
            {$_.OriginalString -like '#*'} {$Root,($_.OriginalString -replace '\A#')}
            {$_.IsFile} {(Get-Content $_.LocalPath -Raw |ConvertFrom-Json -AsHashtable),($_.Fragment -replace '\A*')}
            default {(Invoke-RestMethod $ReferenceUri),($_.Fragment -replace '\A#')}
        }
        return $source |Select-Json $pointer |Import-Reference -Root $source
    }

    filter Import-Reference
    {
        [CmdletBinding()] Param($Root, [Parameter(ValueFromPipeline=$true)] $InputObject)
        if($null -eq $InputObject -or $InputObject -is [bool] -or $InputObject -is [long] -or
            $InputObject -is [double] -or $InputObject -is [string]) {return $InputObject}
        if(($InputObject |ConvertTo-Json -Compress -Depth 100) -notlike '*"$ref":*') {return $InputObject}
        if($InputObject -is [Collections.IList]) {return ,@($InputObject |Import-Reference -Root $Root)}
        if($InputObject -is [Collections.IDictionary])
        {
            if($InputObject.ContainsKey('$ref'))
            {
                return Get-Reference -ReferenceUri $InputObject['$ref'] -Root $Root
            }
            foreach($name in @($InputObject.Keys))
            {
                $InputObject[$name] = Import-Reference -Root $Root -InputObject $InputObject[$name]
            }
            return $InputObject
        }
        if($InputObject.PSObject.Properties.Match('$ref').Count)
        {
            return Get-Reference -ReferenceUri ($InputObject.'$ref') -Root $Root
        }
        foreach($property in $InputObject.PSObject.Properties)
        {
            $name = $property.Name
            $InputObject.$name = Import-Reference -Root $Root -InputObject ($InputObject.$name)
        }
        return $InputObject
    }

    if($Path)
    {
        return Resolve-Path -Path $Path |
            ForEach-Object {$_ |Get-Content -Raw |
                Export-Json -JsonPointer $JsonPointer -Compress:$Compress}

    }
}
End
{
    $root = $InputObject -is [string] ? ($InputObject |ConvertFrom-Json -AsHashtable) : $InputObject
    $selection = $root |Select-Json $JsonPointer
    return $selection |Import-Reference -Root $root |ConvertTo-Json -Depth 100 -Compress:$Compress
}

}

function Merge-Json
{
<#
.SYNOPSIS
Create a new JSON string by recursively combining the properties of JSON strings.
 
.INPUTS
System.String of JSON to combine.
 
.OUTPUTS
System.String of JSON combining the inputs.
 
.FUNCTIONALITY
Json
 
.LINK
ConvertFrom-Json
 
.LINK
ConvertTo-Json
 
.EXAMPLE
'{"a":1,"b":{"u":3},"c":{"v":5}}','{"a":{"w":8},"b":2,"c":{"x":6}}' |Merge-Json
 
{
    "a": {
            "w": 8
        },
    "b": 2,
    "c": {
            "v": 5,
            "x": 6
        }
}
#>


[CmdletBinding()][OutputType([string])] Param(
<#
JSON string to combine. Descendant properties are recursively merged.
Primitive values are overwritten by any matching ones in the new JSON string.
#>

[Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromRemainingArguments=$true)][string[]] $InputObject,
# Omits white space and indented formatting in the output string.
[switch]$Compress
)
Begin {$value = [pscustomobject]@{}}
#TODO: Add or replace dependency.
Process {$value = $value,($InputObject |ConvertFrom-Json) |Merge-PSObject}
End {$value  |ConvertTo-Json -Compress:$Compress}

}

function Resolve-JsonPointer
{
<#
.SYNOPSIS
Returns matching JSON Pointer paths, given a JSON Pointer path with wildcards.
 
.INPUTS
System.String containing JSON, or
System.Collections.Hashtable parsed from JSON, or
System.Management.Automation.PSObject parsed from JSON.
 
.OUTPUTS
System.String of the full JSON Pointer matched.
 
.FUNCTIONALITY
Json
 
.LINK
https://www.rfc-editor.org/rfc/rfc6901
 
.LINK
ConvertFrom-Json
 
.EXAMPLE
'{a:1}' |Resolve-JsonPointer /*
 
/a
 
.EXAMPLE
'[8, 7, 6]' |Resolve-JsonPointer /-
 
/3
 
.EXAMPLE
Resolve-JsonPointer /powershell.*.preset -Path ./.vscode/settings.json
 
/powershell.codeFormatting.preset
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": 7}}}' |Resolve-JsonPointer /*/ZZ?ZZ/AD?BC
 
/b/ZZ~1ZZ/AD~0BC
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": 7}}}' |Resolve-JsonPointer /[bc]/ZZ?ZZ
 
/b/ZZ~1ZZ
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": [7, 8, 9]}}}' |ConvertFrom-Json |Resolve-JsonPointer /?/ZZ*/*BC/-
 
/b/ZZ~1ZZ/AD~0BC/3
 
.EXAMPLE
Resolve-JsonPointer /* -Path .\test\data\sample-openapi.json -IncludePath
 
Path Pointer
---- -------
A:\Scripts\test\data\sample-openapi.json /openapi
A:\Scripts\test\data\sample-openapi.json /info
A:\Scripts\test\data\sample-openapi.json /tags
A:\Scripts\test\data\sample-openapi.json /paths
A:\Scripts\test\data\sample-openapi.json /components
#>


[CmdletBinding()][OutputType([string])] Param(
<#
The full path name of the property to get, as a JSON Pointer, modified to support wildcards:
~0 = ~ ~1 = / ~2 = ? ~3 = * ~4 = [
#>

[Parameter(Position=0)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~[0-4])*)\z')]
[string] $JsonPointer = '',
# The JSON (string or parsed object/hashtable) to get the value from.
[Parameter(ParameterSetName='InputObject',ValueFromPipeline=$true)] $InputObject,
# A JSON file to update.
[Parameter(ParameterSetName='Path',Mandatory=$true)][string] $Path,
# Indicates that the source file path should be included in the output, if available.
[switch] $IncludePath
)
Begin
{
    [string[]] $jsonpath = switch($JsonPointer) { '' {,@()}
        default {,@($_ -replace '\A/' -replace '~4','[[]' -replace '~3','[*]' -replace '~2','[?]' -split '/' -replace '~1','/' -replace '~0','~')} }

    function Resolve-Segment
    {
        [CmdletBinding()] Param(
        [Parameter(Position=0)] $InputObject,
        [Parameter(Position=1)][string] $Segment,
        [Parameter(Position=2)][string[]] $Segments,
        [Parameter(Position=3)][string] $JsonPointer
        )
        $pointer = "$JsonPointer/$($segment -replace '~','~0' -replace '/','~1')"
        if($InputObject -is [array])
        {
            if($Segment -eq '-') {return Resolve-Pointer -InputObject $null -Segments $Segments -JsonPointer "$JsonPointer/$($InputObject.Count)"}
            elseif(![int]::TryParse($Segment,[ref]$Segment)) {return}
            elseif($InputObject.Length -le $Segment) {return}
            else {return Resolve-Pointer -InputObject ($InputObject[$Segment]) -Segments $Segments -JsonPointer $pointer}
        }
        elseif($InputObject -is [Collections.IDictionary])
        {
            if(!$InputObject.ContainsKey($Segment)) {return}
            else {return Resolve-Pointer -InputObject ($InputObject[$Segment]) -Segments $Segments -JsonPointer $pointer}
        }
        else
        {
            return $InputObject.PSObject.Properties.Match($Segment) |
                Select-Object -ExpandProperty Value |
                Resolve-Pointer -Segments $Segments -JsonPointer $pointer
        }
    }

    function Resolve-Wildcard
    {
        [CmdletBinding()] Param(
        [Parameter(Position=0)] $InputObject,
        [Parameter(Position=1)][string] $Segment,
        [Parameter(Position=2)][string[]] $Segments,
        [Parameter(Position=3)][string] $JsonPointer
        )
        if($InputObject -is [array])
        {
            return 0..($InputObject.Length-1) -like $segment |ForEach-Object {
                $pointer = "$JsonPointer/$_"
                Resolve-Pointer -InputObject $InputObject[$_] -Segments $Segments -JsonPointer $pointer}
        }
        elseif($InputObject -is [Collections.IDictionary])
        {
            return $InputObject.Keys -like $segment |ForEach-Object {
                $pointer = "$JsonPointer/$($_ -replace '~','~0' -replace '/','~1')"
                Resolve-Pointer -InputObject $InputObject[$_] -Segments $Segments -JsonPointer $pointer}
        }
        else
        {
            return $InputObject.PSObject.Properties.Match($segment) |ForEach-Object {
                $pointer = "$JsonPointer/$($_.Name -replace '~','~0' -replace '/','~1')"
                Resolve-Pointer -InputObject $_.Value -Segments $Segments -JsonPointer $pointer}
        }
    }

    filter Resolve-Pointer
    {
        [CmdletBinding()] Param(
        [Parameter(ValueFromPipeline=$true)] $InputObject,
        [string[]] $Segments,
        [string] $JsonPointer = ''
        )
        if(!$Segments.Count) {return $JsonPointer}
        $segment,$Segments = $Segments
        if($null -eq $Segments) {[string[]]$Segments = @()}
        if($segment -match '[?*[]') {Resolve-Wildcard -InputObject $InputObject -Segment $segment -Segments $Segments -JsonPointer $JsonPointer}
        else {Resolve-Segment -InputObject $InputObject -Segment $segment -Segments $Segments -JsonPointer $JsonPointer}
    }
}
Process
{
    if($Path)
    {
        if($IncludePath)
        {
            return Resolve-Path -Path $Path -PipelineVariable file |
                Get-Content -Raw |
                ForEach-Object {Resolve-Pointer -InputObject ($_ |ConvertFrom-Json -AsHashtable) -Segments $jsonpath} |
                ForEach-Object {[pscustomobject]@{Path=$file.Path; Pointer=$_}}
        }
        else
        {
            return Resolve-Path -Path $Path |
                Get-Content -Raw |
                ForEach-Object {Resolve-Pointer -InputObject ($_ |ConvertFrom-Json -AsHashtable) -Segments $jsonpath}
        }
    }
    if($null -eq $InputObject) {return}
    if($InputObject -is [string])
    {
        return Resolve-Pointer -InputObject ($InputObject |ConvertFrom-Json -AsHashtable) -Segments $jsonpath
    }
    if(!$jsonpath.Length) {return $JsonPointer}
    return Resolve-Pointer -InputObject $InputObject -Segments $jsonpath
}

}

function Select-Json
{
<#
.SYNOPSIS
Returns a value from a JSON string or file.
 
.INPUTS
System.String containing JSON, or
System.Collections.Hashtable parsed from JSON, or
System.Management.Automation.PSObject parsed from JSON.
 
.OUTPUTS
System.Boolean, System.Int64, System.Double, System.String,
System.Management.Automation.PSObject, or
System.Management.Automation.OrderedHashtable (or null) selected from JSON.
 
.FUNCTIONALITY
Json
 
.LINK
https://www.rfc-editor.org/rfc/rfc6901
 
.LINK
ConvertFrom-Json
 
.EXAMPLE
'true' |Select-Json # default selection is entire parsed JSON document
 
True
 
.EXAMPLE
'{"":3.14}' |Select-Json /
 
3.14
 
.EXAMPLE
Select-Json /powershell.codeFormatting.preset -Path ./.vscode/settings.json
 
Allman
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": 7}}}' |Select-Json /b/ZZ~1ZZ
 
Name Value
---- -----
AD~BC 7
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": 7}}}' |ConvertFrom-Json |Select-Json /b/ZZ~1ZZ
 
AD~BC
-----
    7
 
.EXAMPLE
'{"a":1, "b": {"ZZ/ZZ": {"AD~BC": 7}}}' |Select-Json /b/ZZ~1ZZ |ConvertTo-Json -Compress
 
{"AD~BC":7}
 
.EXAMPLE
'{d:{a:{b:1,c:{"$ref":"#/d/c"}},c:{d:{"$ref":"#/d/two"}},two:2}}' |Select-Json /*/a/c/* -FollowReferences
 
2
 
.EXAMPLE
Resolve-Path $env:LOCALAPPDATA/Packages/*WindowsTerminal*/LocalState/settings.json |Get-Item |Get-Content -Raw |Select-Json /profiles/list/*/name
 
PowerShell
Windows PowerShell
Ubuntu
F# Interactive
F# REPL
C# REPL
Command Prompt
Media Server (ssh)
Azure Cloud Shell
Developer Command Prompt for VS 2022
Developer PowerShell for VS 2022
#>


[CmdletBinding()][OutputType([bool],[long],[double],[string],[Management.Automation.OrderedHashtable])] Param(
<#
The full path name of the property to get, as a JSON Pointer,
modified to support wildcards: ~0 = ~ ~1 = / ~2 = ? ~3 = * ~4 = [
#>

[Parameter(Position=0)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~[0-4])*)\z')]
[string] $JsonPointer = '',
# The JSON (string or parsed object/hashtable) to get the value from.
[Parameter(ParameterSetName='InputObject',ValueFromPipeline=$true)] $InputObject,
# A JSON file to update.
[Parameter(ParameterSetName='Path',Mandatory=$true)][string] $Path,
# Indicates that references should be followed.
[Alias('FollowRefs','References')][switch] $FollowReferences
)
Begin
{
    [string[]] $jsonpath = switch($JsonPointer) { '' {,@()}
        default {,@($_ -replace '\A/' -replace '~4','[[]' -replace '~3','[*]' -replace '~2','[?]' -split '/' -replace '~1','/' -replace '~0','~')} }

    function Get-ReferenceUri
    {
        [CmdletBinding()] Param($InputObject)
        if($InputObject -is [array]) {return}
        if($InputObject -is [Collections.IDictionary] -and $InputObject.ContainsKey('$ref')) {return $InputObject['$ref']}
        if($InputObject.PSObject.Properties.Match('$ref').Count) {return $InputObject.'$ref'}
    }

    function Get-Reference
    {
        [CmdletBinding()] Param([uri] $ReferenceUri, $Root)
        $source,$pointer = switch($ReferenceUri)
        {
            {$null -eq $_} {$Root,''; continue}
            {$_.OriginalString -like '#*'} {$Root,($_.OriginalString -replace '\A#')}
            {$_.IsFile} {(Get-Content $_.LocalPath -Raw |ConvertFrom-Json -AsHashtable),($_.Fragment -replace '\A*')}
            default {(Invoke-RestMethod $ReferenceUri),($_.Fragment -replace '\A#')}
        }
        return $source |Select-Json $pointer
    }

    function Select-Segment
    {
        [CmdletBinding()] Param([Parameter(Position=0)] $InputObject, [Parameter(Position=1)][string] $Segment)
        if($InputObject -is [array])
        {
            if(![int]::TryParse($Segment,[ref]$Segment)) {return}
            elseif($InputObject.Length -le $Segment) {return}
            else {return,$InputObject[$Segment]}
        }
        elseif($InputObject -is [Collections.IDictionary])
        {
            if(!$InputObject.ContainsKey($Segment)) {return}
            else {return,$InputObject[$Segment]}
        }
        else
        {
            return,($InputObject.PSObject.Properties.Match($Segment) |
                Select-Object -ExpandProperty Value)
        }
    }

    function Select-Wildcard
    {
        [CmdletBinding()] Param([Parameter(Position=0)] $InputObject, [Parameter(Position=1)][string] $Segment)
        if($InputObject -is [array])
        {
            return 0..($InputObject.Length-1) -like $Segment |ForEach-Object {$InputObject[$_]}
        }
        elseif($InputObject -is [Collections.IDictionary])
        {
            return $InputObject.Keys -like $Segment |ForEach-Object {$InputObject[$_]}
        }
        else
        {
            return $InputObject.PSObject.Properties.Match($Segment) |
                Select-Object -ExpandProperty Value
        }
    }

    filter Select-Pointer
    {
        [CmdletBinding()] Param(
        [Parameter(ValueFromPipeline=$true)] $InputObject,
        [string[]] $Segments
        )
        if($FollowReferences)
        {
            $refUri = Get-ReferenceUri $InputObject
            if($refUri) {return Get-Reference -ReferenceUri $refUri -Root $Script:Root |Select-Pointer -Segments $Segments}
        }
        if(!$Segments.Count) {return $InputObject}
        $segment,$tail = $Segments
        if($null -eq $tail) {[string[]]$tail = @()}
        if($segment -match '[?*[]') {Select-Wildcard $InputObject $segment |Select-Pointer -Segments $tail}
        else {Select-Segment $InputObject $segment |Select-Pointer -Segments $tail}
    }
}
Process
{
    if($Path)
    {
        return Resolve-Path -Path $Path |
            Get-Content -Raw |
            ForEach-Object {$_ |ConvertFrom-Json -AsHashtable |
                Select-Json -JsonPointer $JsonPointer -FollowReferences:$FollowReferences}
    }
    if($null -eq $InputObject) {return}
    if($InputObject -is [string])
    {
        if($InputObject.StartsWith(([char]0xFEFF))) {$InputObject = $InputObject.Substring(1)}
        return Select-Json -JsonPointer $JsonPointer -FollowReferences:$FollowReferences `
            -InputObject ($InputObject |ConvertFrom-Json -AsHashtable -NoEnumerate)
    }
    if(!$jsonpath.Length) {return $InputObject}
    $Script:Root = $InputObject
    return Select-Pointer -InputObject $InputObject -Segments $jsonpath
}

}

function Set-Json
{
<#
.SYNOPSIS
Sets a property in a JSON string or file.
 
.INPUTS
System.String containing JSON.
 
.OUTPUTS
System.String containing updated JSON (unless a file is specified, which is updated).
 
.FUNCTIONALITY
Json
 
.LINK
https://www.rfc-editor.org/rfc/rfc6901
 
.LINK
ConvertFrom-Json
 
.LINK
ConvertTo-Json
 
.LINK
Add-Member
 
.EXAMPLE
'0' |Set-Json -PropertyValue $true
 
true
 
.EXAMPLE
'{}' |Set-Json / $false
 
{
  "": false
}
 
.EXAMPLE
'{}' |Set-Json /~1/~0 3.14
 
{
  "/": {
    "~": 3.14
  }
}
 
.EXAMPLE
'[1, 2, 3]' |Set-Json /1 0
 
[
  1,
  0,
  3
]
 
.EXAMPLE
'[1, 2]' |Set-Json /- 3
 
[
  1,
  2,
  3
]
 
.EXAMPLE
'{a:{b:[1,2]}}' |Set-Json /a/b/- 3
 
{
  "a": {
    "b": [
      1,
      2,
      3
    ]
  }
}
 
.EXAMPLE
'{a:1}' |Set-Json /b/ZZ~1ZZ/AD~0BC 7
 
{
  "a": 1,
  "b": {
    "ZZ/ZZ": {
      "AD~BC": 7
    }
  }
}
 
.EXAMPLE
Set-Json /powershell.codeFormatting.preset Allman -Path ./.vscode/settings.json
 
Sets "powershell.codeFormatting.preset": "Allman" within the ./.vscode/settings.json file.
#>


[CmdletBinding()][OutputType([string])] Param(
<#
The full path name of the property to set, as a JSON Pointer, which separates each nested
element name with a /, and literal / is escaped as ~1, and literal ~ is escaped as ~0.
#>

[Parameter(Position=0)][Alias('Name')][AllowEmptyString()][ValidatePattern('\A(?:|/(?:[^~]|~0|~1)*)\z')]
[string] $JsonPointer = '',
# The value to set the property to.
[Parameter(Position=1,Mandatory=$true)][AllowEmptyString()][AllowEmptyCollection()][AllowNull()]
[Alias('Value')][psobject] $PropertyValue,
# Indicates that overwriting values should generate a warning.
[switch] $WarnOverwrite,
# The JSON string to set the property in.
[Parameter(ParameterSetName='InputObject',Mandatory=$true,ValueFromPipeline=$true)][string] $InputObject,
# A JSON file to update.
[Parameter(ParameterSetName='Path',Mandatory=$true)][string] $Path
)
Begin
{
    [string[]] $jsonpath = switch($JsonPointer) { '' {,@()}
        default {,@($_ -replace '\A/' -split '/' -replace '~1','/' -replace '~0','~')} }
}
Process
{
    if(!$jsonpath.Length)
    {
        if($Path) {$PropertyValue |ConvertTo-Json -Depth 100 |Out-File $Path utf8NoBOM; return}
        else {return $PropertyValue |ConvertTo-Json -Depth 100}
    }
    $object = ($Path ? (Get-Content $Path -Raw) : $InputObject) |ConvertFrom-Json -AsHashtable
    if($null -eq $object) {return}
    $property,$parent = $object,$null
    for($i = 0; $i -lt ($jsonpath.Length-1); $i++)
    {
        $segment = $jsonpath[$i]
        if($property -is [array])
        {
            if($i -eq 0 -and $segment -eq '-') {$property = @{}; $object += $property; $segment = $object.Count}
            if(![int]::TryParse($segment,[ref]$segment)) {throw "Could not use array index $segment"}
            elseif($property.Count -le $segment) {$property = @{}; $object += $property; $segment = $object.Count}
            else {$property,$parent = $property[$segment],$property}
        }
        else
        {
            if(!$property.ContainsKey($segment)) {$property[$segment] = @{}}
            $property,$parent = $property.$segment,$property
        }
        if($property -is [array] -and $i -lt ($jsonpath.Length-2) -and $jsonpath[$i+1] -eq '-')
        { # RFC6091 uses '-' to append to an array
            $property += @{}
            $jsonpath[$i+1] = $property.Count
            $parent.$segment = $property
        }
    }
    $segment = $jsonpath[-1]
    if($property -is [array])
    {
        if($segment -eq '-') {if($jsonpath.Length -eq 1) {$object += $PropertyValue} else {$parent.$($jsonpath[-2]) += $PropertyValue}}
        elseif(![int]::TryParse($segment,[ref]$segment)) {throw "Could not use array index $segment"}
        elseif($property.Count -le $segment) {if($jsonpath.Length -eq 1) {$object += $PropertyValue} else {$parent.$($jsonpath[-2]) += $PropertyValue}}
        else {$property[$segment] = $PropertyValue}
    }
    else
    {
        if($property.ContainsKey($segment) -and $WarnOverwrite) {Write-Warning "Property $JsonPointer overwriting '$($property.$segment)'."}
        $property[$segment] = $PropertyValue
    }
    $value = $object |ConvertTo-Json -Depth 100
    if($Path) {$value |Out-File $Path utf8NoBOM}
    else {return $value}
}

}
Export-ModuleMember -Function Export-Json,Merge-Json,Resolve-JsonPointer,Select-Json,Set-Json