add tag
Jack Douglas
Starting with any arbitrary JSON, how can I list all the leaf nodes with the full path and the value.

I know about `ConvertFrom-Json` as the first step:

```
> @"
{
  "a": "foo",
  "b": [
    1,
    2,
    {
      "d": "bar",
      "e": [
        0,
        1
      ]
    }
  ],
  "f": {
    "g": "baz"
  },
  "h": []
}
"@ | ConvertFrom-Json

a   b                                   f        h
-   -                                   -        -
foo {1, 2, @{d=bar; e=System.Object[]}} @{g=baz} {}
```

I'd like to end up with something like this:

```
".a" = "foo"
".b[0]" = 1
".b[1]" = 2
".b[2].d" = "bar"
".b[2].e[0]" = 0
".b[2].e[1]" = 1
".f.g" = "baz"
```
Top Answer
PeterVandivier
Through judicious use of filtering and [`Get-Member`][1], that's how. I've encapsulated my approach in the utility function [`Get-PropertyValues`][2]. The full source is at the bottom of this post (and at the link), but I'll take the space here to talk through it a bit.

The default output type of [`ConvertFrom-Json`][3] is `PSCustomObject` ([additional][4] [reading][5]). If we stash this output in a variable called `$object`, we can see the following

```
PS> $object | Get-Member


   TypeName: System.Management.Automation.PSCustomObject

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       bool Equals(System.Object obj)
GetHashCode Method       int GetHashCode()
GetType     Method       type GetType()
ToString    Method       string ToString()
a           NoteProperty string a=foo
b           NoteProperty Object[] b=System.Object[]
f           NoteProperty System.Management.Automation.PSCustomObject f=@{g=baz}
h           NoteProperty Object[] h=System.Object[]

[3.39ms] /Users/pvandivier
PS>
```

When enumerating the `$object`, we're going to want to 

1. keep the properties
2. discard the methods
3. recurse into nested objects and arrays

The dot-and-bracket syntax allows us to navigate both the raw JSON document and the PSObject; so we build a path to each leaf node by

1. dot separating keys
2. prepending the path we're already at
3. quotename escaping non-alphanumeric keys
4. indexing into arrays as we find them

I'm not aware of a sensible way to extract the value in the same single pass needed to extract the information needed to build the path. Therefore the constructed path is used to index back into the base `$object` (using [`Invoke-Expression`][6]) and retrieve its own value. The full path and its corresponding value are then returned to the host.

---

### Full source.

```
function Get-PropertyValues {
<#
.SYNOPSIS
    Returns a 1-level deep list of all leaf-level properties and corresponding values of the input object

.DESCRIPTION
    Unwrap a potentially nested PSObject. Return all valid leaf paths an the corresponding values where
    the given path returns either a null value or a scalar value. 

.PARAMETER InputObject
    Mandatory - The object to unwrap

.PARAMETER PathPrefix
    Specifies the path name to use as the root. If not specified, all properties will start with "."

.NOTES
    Inspired by code posted here - https://stackoverflow.com/a/14970081/4709762
#>
    [CmdletBinding()]
    param (
        [Parameter(Mandatory,ValueFromPipeline,Position=0)]
        [psobject]
        $InputObject, 

        [string]
        $PathPrefix
    )

    begin{
        $arrayTypes = @(
            'System.Management.Automation.PSCustomObject',
            'System.Object[]'
        )

        function Get-Level {
            <#
            .DESCRIPTION
                Peel a single layer off the Onion, then do it again
            #>
            [CmdletBinding()]
            param (
                [Parameter()]
                [PSObject]
                $InputObject
            )
            $rootProps = $InputObject | Get-Member | Where-Object { $_.MemberType -match "Property"} 

            $rootProps | ForEach-Object { 
                $propName = $_.Name
    
                if($propName -match '[^0-9a-zA-Z_]' ){
                    $propName = $propName.Replace("'", "''")
                    $propName = "'$propName'"
                }
    
                $propValue = Invoke-Expression "`$InputObject.$propName"

                $propType = if($null -eq $propValue){
                    $null
                } else {
                    $propValue.GetType().ToString()
                }
    
                if($propType -in $arrayTypes){
                    if($propType -eq 'System.Object[]'){
                        $i = 0
                        $propValue | ForEach-Object {
                            if($_.GetType().ToString() -notin $arrayTypes){   
                                [PSCustomObject]@{
                                    Path = "$PathPrefix.$propName[$i]" 
                                    Value = $_
                                }
                            }else{
                                Get-PropertyValues $propValue -PathPrefix "$PathPrefix.$propName[$i]"
                            }
                            $i ++
                        }
                    }else{
                        Get-PropertyValues $propValue -PathPrefix "$PathPrefix.$propName"
                    }
                } else {
                    [PSCustomObject]@{
                        Path = "$PathPrefix.$propName" 
                        Value = $propValue
                    }
                }
            }
        }
    }

    process{
        $props = Get-Level $InputObject
    }

    end{
        return $props
    }
}
```

[1]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-member
[2]: https://gist.github.com/petervandivier/11d9ef2c0e1c5d0728ffc8113cf831cd
[3]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json
[4]: https://stackoverflow.com/a/50196654/4709762
[5]: https://powershellexplained.com/2016-10-28-powershell-everything-you-wanted-to-know-about-pscustomobject/
[6]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-expression

Enter question or answer id or url (and optionally further answer ids/urls from the same question) from

Separate each id/url with a space. No need to list your own answers; they will be imported automatically.