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.