Reputation: 43
What I'm trying to do is take any well-formed JSON file/object and search for a path inside it. If the path isn't found, move on. If it is found, update the value. Once updated, save the updated JSON to the original file.
The catch to this, is the well-formed JSON structure is not known ahead of time. It's possible I might be searching hundreds of .json files on a disk, so the files that don't match any of my search terms can just be ignored.
I'm struggling to wrap my head around how to solve this problem. Most of the examples out there don't have a JSON object with an array for one of the key values, or they don't access the properties dynamically when an array is involved.
This link: Powershell: How to Update/Replace data and values in Json and XML Object shows a (sort of)"real" JSON structure, but the accepted answer relies on knowing what the JSON structure is (the OP didn't ask for help with dynamic pathing).
This link: Set Value of Nested Object Property by Name in PowerShell has something very close, although when an array is in the mix, it doesn't work properly when setting.
Here's some example JSON to use with this problem, though again, the structure is not known before the script runs. I'm looping over a list of files on disk, and executing for each file.
$JSON = ConvertFrom-Json '{
"key1":"key 1 value",
"options":{
"outDir":"./app-dir",
"lib":[
"someLibrary",
"anotherLibrary"
],
"someObjects":[
{
"first":"I am first"
},
{
"second":"I am second"
}
]
}
}'
The string to search this json might look like the following:
$SearchString = 'options.someObjects.first'
Or perhaps, something non-existent like:
$SearchString = 'options.someObjects.foo'
Using the recursive function GetValue from the 2nd article works beautifully for getting (and much more elegant than what I was doing):
function GetValue($object, $key)
{
$p1,$p2 = $key.Split(".")
if($p2) { return GetValue -object $object.$p1 -key $p2 }
else { return $object.$p1 }
}
However, the function SetValue does not work with an array. It returns an error stating "The property 'first' can not be found on this object."
function SetValue($object, $key, $Value)
{
$p1,$p2 = $key.Split(".")
if($p2) { SetValue -object $object.$p1 -key $p2 -Value $Value }
else { $object.$p1 = $Value }
}
I am aware this is because $JSON.options.someObjects is an array, therefore to access the object with the "first" key, the path would be:
$JSON.options.someObjects[0].first
That's the problem I'm having. How do I dynamically iterate over all objects once it reaches a part of the path that needs iterating? That part of the path could be anywhere, or more levels down, etc...
It's strange that powershell will allow you to dynamically iterate through an array when getting the value, but not when trying to set it.
Here's a complete example which demonstrates the entire issue:
#Create JSON:
$JSON = ConvertFrom-Json '{
"key1":"key 1 value",
"options":{
"outDir":"./app-dir",
"lib":[
"someLibrary",
"anotherLibrary"
],
"someObjects":[
{
"first":"I am first"
},
{
"second":"I am second"
}
]
}
}'
$SearchPath = 'options.someObjects.first'
$NewValue = 'I am a new value'
function GetValue($object, $key)
{
$p1,$p2 = $key.Split(".")
if($p2) { GetValue -object $object.$p1 -key $p2 }
else { return $object.$p1 }
}
function SetValue($object, $key, $Value)
{
$p1,$p2 = $key.Split(".")
if($p2) { SetValue -object $object.$p1 -key $p2 -Value $Value }
else { return $object.$p1 = $Value }
}
GetValue -object $JSON -key $SearchPath
SetValue -object $JSON -key $SearchPath -Value $NewValue
I've been searching all kinds of different terms trying to arrive at a good solution for this problem, but so far, no luck. I'm fairly certain I'm not the 1st person to want to do this sort of thing, apologies if I missed the answer somewhere.
Upvotes: 4
Views: 3673
Reputation: 23613
There are two issues with your SetValue
script:
[Object]
) vs an object array
([Object[]]
)You can't return an assignment like return $object.$p1 = $Value
. The assignment itself returns nothing with will result in returning a $Null
to caller.
Besides, if you return the $Object
for each recursive call, you will need to void ($Null = SetValue -object...
) it by each parent caller so that it is only returned by the top caller. but keep in mind that you are actually poking the $NewValue
in the original ($JSON
) object!. If you don't want that, you will need to figure out the top caller and only copy the $Object
at the top level before the recursive call.
You not just dealing with properties containing single objects but each property might potentially contain a collection objects. In fact, the leaf property SomeObject
is an example of this. Meaning that each object in the collection has its own unique set of properties (which could have the same property name as the sibling object):
$JSON = ConvertFrom-Json '{
"key1":"key 1 value",
"options":{
"outDir":"./app-dir",
"lib":[
"someLibrary",
"anotherLibrary"
],
"someObjects":[
{
"first":"I am first"
},
{
"first":"I am first too"
},
{
"second":"I am second"
}
]
}
}'
Note that you might actually encounter a object collection at every level of the Json object.
Since PSv3 you have a feature called Member Enumeration which lets you list these properties in ones, like: ([Object[]]$SomeObject).First
but you can't just set (all) the concerned properties like this: ([Object[]]$SomeObject).First = $Value
. (That is why your SetValue
function doesn't work and your GetValue
function does. Note that it
actually returns two items for the above "I am first too
" Json example).
In other words, you will need to iterate through all the object collections on each level to set the concerned property:
function SetValue($object, $key, $Value)
{
$p1,$p2 = $key.Split(".",2)
if($p2) { $object | ?{$Null -ne $_.$p1} | %{SetValue -object $_.$p1 -key $p2 -Value $Value} }
else { $object | ?{$Null -ne $_.$p1} | %{$_.$p1 = $Value} }
}
SetValue -object $JSON -key $SearchPath -Value $NewValue
$Json | ConvertTo-Json -Depth 5
{
"key1": "key 1 value",
"options": {
"outDir": "./app-dir",
"lib": [
"someLibrary",
"anotherLibrary"
],
"someObjects": [
{
"first": "I am a new value"
},
{
"second": "I am second"
}
]
}
}
Upvotes: 4