Table of Contents
Stringify dynamic child properties in Json with PowerShell
I had a fun one yesterday that I want to share here.
We have a database at work (new job at InSpark), in the database we have a table in which we can create a row ourselves by making a request via a put method. The row cannot contain nested data other than a stringify json (or you’ll receive a 400 request error as it expects nested Json objects to be stringified).
But what if the data that you want to send to the row in the database is dynamic? Hypothetically speaking, of course.
What we’re going to do is create a PowerShell function that transforms a PowerShell object into a json with the top properties and stringified json per property below that.
And then we’re going to create a function to transform the data back into a PowerShell object.
What is the difference between ConvertTo-Json and ‘Stringify’?
When you have a PowerShell Object and you convert it to a Json string, it becomes a Json with a depth. The Rest API can’t handle depth.
So instead of a depth, we make sure that the depth is treated as a loose string.
formatted Json shows depth correctly:
{
"I": {
"am": {
"a": {
"json": {
"file": "I am a json file"
}
}
}
}
}
So, what we want to get to is that he sees the depth as 1 whole string.
Minifying the Json shows us where we want it to go, but the Rest API will still see this as nested properties.
{
"I": {
{"am":{"a":{"json":{"file":"I am a json file"}}}}
}
}
So, ultimately you’ll want to have something like double quotes around it so it sees it as 1 string, but the Json would be incorrect then.
Let’s create a function in PowerShell that will turn the depth in to stringified Json
Our base is above (the formatted Json).
We call the function ConvertTo-StringifiedJson
.
The function contains 1 parameter. That is the PowerShell object, or already formatted Json.
function ConvertTo-StringifiedJson {
param (
[Parameter(Mandatory = $true)]
[object] $Body
)
}
Since we don’t know if the $Body
is already Json or an object, we add a try {} catch {}
block where we try to convert the $Body
to an object.
If this fails, it automatically jumps to the catch {}
and we convert the object to Json first to easily convert it to a hashtable.
function ConvertTo-StringifiedJson {
param (
[Parameter(Mandatory = $true)]
[object] $Body
)
try {
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
catch {
$Body = $Body | ConvertTo-Json -Compress
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
}
And why a hashtable? Because then you can easily go through the properties using the keys of the hashtable.
We have now laid the foundation for looping through the object with a foreach
loop, making it dynamic automatically.
Below is the code for this, I also added comments to make it easier to understand what happens on each step (and in short explained below).
function ConvertTo-StringifiedJson {
param (
[Parameter(Mandatory = $true)]
[object] $Body
)
try {
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
catch {
$Body = $Body | ConvertTo-Json -Depth 99 -Compress
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
# after rebuilding the json we will add the result to ModifiedJson
$ModifiedJson = @{}
# iterate over the keys of the parsed json
foreach ($Key in $ParsedJson.Keys) {
# get the value of the key
$Value = $ParsedJson[$Key]
# if the value is a hashtable or a custom object
if ($Value -is [PSCustomObject] -or $Value -is [hashtable]) {
# convert the value to json
$ValueJson = $Value | ConvertTo-Json -Depth 99 -Compress
# escape the json quotes with the default json escape character
$EscapedJson = $ValueJson -replace '"', '\"'
# add the escaped json to the modified json
$ModifiedJson[$Key] = $EscapedJson
}
else {
# if the value is not a hashtable or a custom object, add it to the modified json as is
$ModifiedJson[$Key] = $Value
}
}
# convert the modified json to a stringified json
$FinalJson = $ModifiedJson | ConvertTo-Json -Depth 99 -Compress
# return the stringified json
return $FinalJson
}
Because we have a hashtable we use the keys to retrieve the value.
After we retrieve the value we can check if this is an object (or hashtable), because there is no point in converting a string to a stringified string.
Then we add the $EscapedJson
to a new hashTable $ModifiedJson
.
And when the foreach loop is finished we convert the newly created hashtable $ModifiedJson
back to Json. (and for the sake of it with the -Compress
parameter).
And then you have a Json whose child properties have become a stringified json.
And if we now give the formatted Json as Body to the function, this is the result:
$Json = '
{
"I": {
"am": {
"a": {
"json": {
"file": "I am a json file"
}
}
}
}
}
'
ConvertTo-StringifiedJson -Body $Json
{"I":"{\\\"am\\\":{\\\"a\\\":{\\\"json\\\":{\\\"file\\\":\\\"I am a json file\\\"}}}}"}
And then you suddenly wonder why there are 3 ‘\’ backslashes if we only replace it once?
That’s because the ConvertTo-Json
also escapes the characters.
This is nice if you have to convert the stringified Json back to an object.
This ensures that quotes in the string itself are also escaped and your data remains correct.
So, let’s say this is our Json, with double quotes around the word Json (this has to be in a PowerShell object otherwise you had to escape the characters already).
$Json = @{
I = @{
am = @{
a = @{
json = @{
file = 'I am a "json" file'
}
}
}
}
}
ConvertTo-StringifiedJson -Body $Json
{"I":"{\\\"am\\\":{\\\"a\\\":{\\\"json\\\":{\\\"file\\\":\\\"I am a \\\\\"json\\\\\" file\\\"}}}}"}
As you can see, the word Json received an extra backslash.
I’ll come back to this when converting the json back to a PowerShell object.
Let’s create a PowerShell function that converts the Json back to a PowerShell object!
Okay, let’s use the last result with the double quotes around Json as Input.
{"I":"{\\\"am\\\":{\\\"a\\\":{\\\"json\\\":{\\\"file\\\":\\\"I am a \\\\\"json\\\\\" file\\\"}}}}"}
First, we create a new function and call it ConvertFrom-StringifiedJson
.
Since we’re going to use a hashtable again, I’ll convert the $Body
to a hashtable.
function ConvertFrom-StringifiedJson {
param (
[Parameter(Mandatory = $true)]
[string] $Body
)
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
We are now going to go through the properties again with a foreach
loop and see if the value matches a regex value.
function ConvertFrom-StringifiedJson {
param (
[Parameter(Mandatory = $true)]
[string] $Body
)
$ParsedJson = $Body | ConvertFrom-Json -AsHashtable -ErrorAction Stop
# after rebuilding the json to an object we will add the result to ModifiedJson
$RestoredHashTable = @{}
# iterate over the keys of the parsed json
foreach ($Key in $ParsedJson.Keys) {
# get the value of the key
$Value = $ParsedJson[$Key]
# match if the value is a stringified json object
if ($Value -match "^\{.*\}$") {
# unescape the unnecessary json quotes
$UnescapedJson = $Value.Replace('\"', '"')
# convert the unescaped json to an object
$RestoredHashTable[$Key] = $UnescapedJson | ConvertFrom-Json -ErrorAction Stop
}
else {
# if the value is not a stringified json object, add it to the restored hashtable as is (a string)
$RestoredHashTable[$Key] = $Value
}
}
# return the restored hashtable
return $RestoredHashTable
}
The regex basically means that it only takes values that start and end with {}, otherwise it would just be a string that we don’t have to convert to a PowerShell object.
At the bottom of the blog is the Regex explained by Gemini.
In the function we now replace the \"
with a "
.
After removing the unnecessary escape characters, it is a Json with child properties again and we can convert it to an object.
After it is converted, we add the value back to a new hashtable. After the foreach
loop is finished, we have a hashtable with the correct values in it.
Below is an example, and as you can see the Converted Json is now a hashtable again, with double quotes around Json.
$ConvertedJson = ConvertTo-StringifiedJson -Body $Json
$ConvertedJson
{"I":"{\\\"am\\\":{\\\"a\\\":{\\\"json\\\":{\\\"file\\\":\\\"I am a \\\\\"json\\\\\" file\\\"}}}}"}
ConvertFrom-StringifiedJson -Body $ConvertedJson
Name Value
---- -----
I @{am=}
$Hashtable.I.am.a.json.file
I am a "json" file
The regex for converting the value back to PowerShell objects – explained by Gemini
Let’s break down the regex ^\{.*\}$
:
^
(Caret):- This is an anchor that asserts the position at the start of the string. In other words, the pattern that follows must begin at the very beginning of the string.
\{
(Escaped Opening Curly Brace):- The curly brace
{
is a special character in regex (used for quantifiers). To match a literal opening curly brace, it needs to be escaped with a backslash\
. So, this part of the regex matches a literal opening curly brace.
- The curly brace
.*
(Dot Star):.
(Dot): This matches any single character (except newline characters by default).*
(Asterisk): This is a quantifier that means “zero or more occurrences” of the preceding character or group.- Therefore,
.*
matches any sequence of zero or more characters. It’s a “greedy” match, meaning it will try to match as much as possible.
\}
(Escaped Closing Curly Brace):- Similar to the opening curly brace, the closing curly brace
}
also needs to be escaped with a backslash\
to match a literal closing curly brace.
- Similar to the opening curly brace, the closing curly brace
$
(Dollar Sign):- This is another anchor that asserts the position at the end of the string. The pattern preceding it must end at the very end of the string.
In summary:
The entire regex ^\{.*\}$
matches any string that:
- Starts with an opening curly brace
{
. - Contains any sequence of zero or more characters in between.
- Ends with a closing curly brace
}
.
Examples:
{}
– Matches{abc}
– Matches{123!@#}
– Matches{This is a string}
– Matchesabc{}
– Does not match (because it doesn’t start with{
){abc
– Does not match (because it doesn’t end with}
)abc
– Does not match.
Made with Gemini