I have a function I was trying to build in PowerShell to explore an unknown JSON structure and am having trouble with the recursion failing after the second run.
Here is my code:
#Example string taken randomly from https://hackersandslackers.com/extract-data-from-complex-json-python/
$TestJson = @"
{
"destination_addresses": [
"Philadelphia, PA, USA"
],
"origin_addresses": [
"New York, NY, USA"
],
"rows": [{
"elements": [{
"distance": {
"text": "94.6 mi",
"value": 152193
},
"duration": {
"text": "1 hour 44 mins",
"value": 6227
},
"status": "OK"
}]
}],
"status": "OK"
}
"@
$TestObj = ConvertFrom-Json $TestJson
Function Map-NestedPSObj {
[CmdletBinding()]
Param (
[Parameter(Mandatory=$True,ValueFromPipeline = $true)]
$Obj,
[Parameter(Mandatory=$False,ValueFromPipeline = $true)]
$Iteration=0
)
Begin {
#Get type of object passed
$ObjTypeName = (($Obj).GetType()).Name
#Create next iteration #, to pass to recursive call
$NextIteration = $Iteration 1
}
Process {
#Switch statement to process according to which obj type was passed to script
Switch ($ObjTypeName)
{
#Run if $obj is a PSCustomObject
"PSCustomObject" {
#Loop through NoteProperties
foreach($rootProperty in @($Obj.psobject.properties | where-object {$_.MemberType -eq "NoteProperty"}) )
{
#Get name of current NoteProperty
$PropertyName = $rootProperty.Name
#Indent lines based on how many loops have been run
if($Iteration -gt 0){
1..$Iteration | foreach {
$PropertyName = "`t$PropertyName"
}
}
#Show PropertyName
write-host $PropertyName
#Drill down recursively
Map-NestedPSObj $rootProperty.value $NextIteration
}
}
#Run if passed obj is an array
"Object[]" {
#Get Property Name
$PropertyName = ($Obj | Get-Member | Where-Object { $_.MemberType -eq "NoteProperty" }).Name
#Indent PropertyName
if($Iteration -gt 0){
1..$Iteration | foreach {
$PropertyName = "`t$PropertyName"
}
}
#Output PropertyName
Write-Host $PropertyName
#Drill down recursively
Map-NestedPSObj $Obj.$PropertyName $NextIteration
}
}
}
}
Map-NestedPSObj $TestObj
When I run this I should get the following output:
destination_addresses
origin_addresses
rows
elements
distance
text
value
duration
text
value
status
status
But instead I get this:
destination_addresses
Map-NestedPSObj : Cannot bind argument to parameter 'Obj' because it is null.
At line:80 char:29
Map-NestedPSObj $Obj.$PropertyName $NextIteration
~~~~~~~~~~~~~~~~~~
CategoryInfo : InvalidData: (:) [Map-NestedPSObj], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Map-NestedPSObj
origin_addresses
Map-NestedPSObj : Cannot bind argument to parameter 'Obj' because it is null.
At line:80 char:29
Map-NestedPSObj $Obj.$PropertyName $NextIteration
~~~~~~~~~~~~~~~~~~
CategoryInfo : InvalidData: (:) [Map-NestedPSObj], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Map-NestedPSObj
rows
elements
Map-NestedPSObj : Cannot bind argument to parameter 'Obj' because it is null.
At line:80 char:29
Map-NestedPSObj $Obj.$PropertyName $NextIteration
~~~~~~~~~~~~~~~~~~
CategoryInfo : InvalidData: (:) [Map-NestedPSObj], ParameterBindingValidationException
FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Map-NestedPSObj
status
I do plan on eventually trying to add type data for each level, but for now I would be fine with just getting the expected output! Can anyone see what I am doing wrong? Been banging my head against the wall on this for several hours now, any help is much appreciated.
*Edit For those stumbling on this in the future, there is a MUCH more elegant function posted below by Sage Pourpre that does everything I was attempting with this formula and more. However, the answer to the question I asked was to change:
Map-NestedPSObj $rootProperty.value $NextIteration
to:
Map-NestedPSObj $($rootProperty.value) $NextIteration
Answer was provided by /u/jducktape on r/powershell
CodePudding user response:
Regarding your code, I can see several issues, which I already highlighted in the 2 comments I left to your question.
Here is a simple function I made while looking at yours to produce the desired result.
# Your json (compressed version)
$TestJson = '{"destination_addresses":["Philadelphia, PA, USA","Montreal, QC, CANADA"],"origin_addresses":["New York, NY, USA"],"rows":[{"elements":[{"distance":{"text":"94.6 mi","value":152193},"duration":{"text":"1 hour 44 mins","value":6227},"status":"OK"}]}],"status":"OK"}'
$TestObj = ConvertFrom-Json $TestJson
Function Show-Json {
[CmdletBinding()]
Param($InputObject, [Switch]$ShowValues, $Depth = 0)
$IndentChar = " "
#$IndentChar = "`t" # For me "`t" was too much indent
switch ($InputObject) {
{ $_ -is [pscustomobject] } {
$Properties = Get-Member -InputObject $_ -MemberType NoteProperty
Foreach ($P in $Properties.Name) {
Write-Host ($IndentChar * $Depth $P)
Show-Json -InputObject $InputObject.$P -Depth ($Depth 1) -ShowValues:$ShowValues
}
break
}
Default {
if ($ShowValues) { Write-Host ($IndentChar * $Depth $_) }
}
}
}
No values
Show-json $TestObj
Output
destination_addresses
origin_addresses
rows
elements
distance
text
value
duration
text
value
status
status
With values
Show-Json -InputObject $TestObj -ShowValues
Output
destination_addresses
Philadelphia, PA, USA
Montreal, QC, CANADA
origin_addresses
New York, NY, USA
rows
elements
distance
text
94.6 mi
value
152193
duration
text
1 hour 44 mins
value
6227
status
OK
status
OK
Bonus tip
Instead of:
if($Iteration -gt 0){
1..$Iteration | foreach {
$PropertyName = "`t$PropertyName"
}
}
To do the indent, you can use multiply on a string and replace your whole thing by:
"`t" * 5 $PropertyName`
or (same but ditching the string concatenation
for a subexpression
"$("`t" * 5)$PropertyName"`
In both case, you don't need the if
anymore since multiplying a string by 0 will give you an empty string.