Home > Net >  Formula to iterate through complex JSON string, script erroring after second recursion
Formula to iterate through complex JSON string, script erroring after second recursion

Time:03-10

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.

  • Related