Note:
- Generally speaking,
null is a legitimate JSON value that becomes $null when PowerShell parses JSON into custom objects with its ConvertFrom-Json cmdlet, which, in effect, is also built into Invoke-RestMethod.
If you really need to transform the null values into "null" values - i.e. strings - in your JSON, the most robust - but not fast - approach is to:
- Let
ConvertFrom-Json parse your JSON into custom objects...
- ... then walk the internal structure of these objects to look for
$null property values and replace them with string 'null'...
- See the bottom section for generalized helper function
Edit-LeafProperty that encapsulates this behavior.
- ... and convert the modified objects back to JSON.
# Parse JSON into custom object(s).
$fromJson = ConvertFrom-Json @'
{
"Name": {
"Tag1": "server1",
"Tag2": null,
"Tag3": "web"
}
}
'@
# Transform the object(s) in place.
$fromJson | ForEach-Object {
# Helper script block that walks the object graph
$sb = {
foreach ($el in @($args[0])) {
if ($el -is [System.Collections.IEnumerable]) { # Nested array
foreach ($subEl in $el) { & $sb $subEl } # Recurse
}
elseif ($el -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $el.psobject.Properties) {
# If the property value is $null, replace it with string 'null'
if ($null -eq $prop.Value) { $prop.Value = 'null' }
elseif ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
& $sb $prop.Value # Recurse
}
}
}
}
}
# Call the helper script block with the input object.
& $sb $_
}
# Convert the transformed object(s) back to JSON
ConvertTo-Json $fromJson
Output (note the "null"):
{
"Name": {
"Tag1": "server1",
"Tag2": "null",
"Tag3": "web"
}
}
Caveat re single-element arrays:
If your input JSON is an array ([ ... ]) that happens to contain just one element, in PowerShell (Core) 7+ you need to add -NoEnumerate to the ConvertFrom-Json call in order to parse the JSON into a PowerShell array - otherwise, you'll get just get that one element itself, not wrapped in an array. (This isn't necessary in Windows PowerShell).[1]
With generalized helper function Edit-LeafProperty:
If you define the helper function below (before running the following code), the code simplifies to the following:
@'
{
"Name": {
"Tag1": "server1",
"Tag2": null,
"Tag3": "web"
}
}
'@ | ConvertFrom-Json |
Edit-LeafProperty -PassThru { if ($null -eq $_.Value) { $_.Value = 'null' } } |
ConvertTo-Json
Note:
Each leaf property (an object property that doesn't itself contain another, nested [pscustomobject]) is passed to the specified script block ({ ... }) bound to $_, where its .Value (and, if needed, .Name) property can be examined and updated, as needed.
-PassThru passes each modified object through (outputs it after modification), so that the result can directly be piped to ConvertTo-Json
- The usual caveat re potentially unexpected truncation of the output applies: use
-Depth as needed for object graphs more than 2 levels deep - see this post
The caveats re array preservation in PowerShell (Core) apply as before, with an additional twist:
- Use
ConvertFromJson -NoEnumerate to preserve (potentially nested) single-element arrays as such.
- Because of the use of an intermediate streaming command -
Edit-LeafProperty - use ConvertTo-Json -AsArray if you need to guarantee that the output JSON is an array, even when containing just one element.
Note that the function is designed to work with [pscustomobject] ([System.Management.Automation.PSCustomObject]) graphs only, such as returned by ConvertFrom-Json and ConvertFrom-Csv.
Edit-LeafProperty source code:
function Edit-LeafProperty {
[CmdletBinding(PositionalBinding = $false)]
param(
[Parameter(Mandatory, Position = 0)]
[scriptblock] $ScriptBlock,
[Parameter(Mandatory, ValueFromPipeline)]
[AllowNull()]
$InputObject,
[switch] $PassThru
)
begin {
# Helper script block that walks the object graph
$sb = {
foreach ($el in @($args[0])) {
if ($el -is [System.Collections.IEnumerable]) { # Nested array
foreach ($subEl in $el) { & $sb $subEl } # Recurse
}
elseif ($el -is [System.Management.Automation.PSCustomObject]) {
foreach ($prop in $el.psobject.Properties) {
if ($prop.Value -is [System.Management.Automation.PSCustomObject]) {
& $sb $prop.Value # Recurse
}
else {
# Invoke the leaf-property processing script block with
# the property at hand bound to $_
ForEach-Object $ScriptBlock -InputObject $prop
}
}
}
}
}
}
process {
& $sb $InputObject # Walk the input object at hand.
if ($PassThru) { $InputObject } # Pass it through, if requested.
}
}
[1] See this answer for what prompted this change in behavior.