Search code examples
pythonpowershelldictionaryhashtablepowershell-5.0

How to traverse through JSON data structure in PowerShell?


I am new to PowerShell, learning it on the fly coming from Python background.

I am pulling data from another tool where the data is retrieved through a REST call.

METERS variable is stored in this format in the source.

{
    "500 HR": 500,
    "1000 HR": 1000,
    "2000 HR": 2000
}

PowerShell code

#REST call to source
$meter=@{}
Foreach ($item in $origresult.Items)  {
$result = (Invoke-RestMethod -Uri $url  -Headers $headers -Method GET -ContentType application/json -ErrorVariable RespErr)
$meter.Add($item.Name,$result.Value)
}
Write-Host ($meter | Out-String) -ForegroundColor Red

This is the Output

Name                           Value
----                           -----
LastUploaded                   2020-12-29T06:38:02
IsEnabled                      1       
METERS                         {...
ORGID                          WHS

How do I retrieve METERS and traverse through the dictionary? I tried this so far. Python spoiled me with its simple data structures, PowerShell is not so straightforward unless there is a simpler way to do this.

$mymeters = $meter.METERS | ConvertFrom-Json
Write-Host ($mymeters | Out-String) -ForegroundColor Yellow

Output

500 HR   : 500
1000 HR  : 1000
2000 HR  : 2000

Here are somethings I have tried so far -

$mymeters = [ordered]@{}
" Here is the item $mymeters.Get_Item(500 HR)" #my silly attempt!
# Looping is a no go either - it says specialized ordered dictionary
foreach ($ind in $mymeters) {
" --> $ind"
}

Output

 Here is the item System.Collections.Specialized.OrderedDictionary.Get_Item(500 HR)
 --> System.Collections.Specialized.OrderedDictionary

I might be missing something real basic but I am unable to figure it out on my own! Any help is really appreciated. All I want is to traverse on the METERS hashtable/dictionary and call a function.


Solution

  • Before getting into the meat of it, let's review a few PowerShell syntax basics and see if we can re-use some of your Pythonic intuition :)

    Member access

    Just like in Python, you can reference the properties of an object by name using the . member access operator - for non-contiguous names, use quotes:

    $mymeters = $meter.METERS | ConvertFrom-Json
    $mymeters.'500 HR'  # evaluates to `500`
    

    String expressions

    String literals in PowerShell comes in two distinct flavours:

    • Single-quoted strings ('Hello World!')
      • These are verbatim string literals, and the only supported escape sequence is '' (literal single-quote)
    • Double-quoted strings ("Hello World!")
      • These are expandable string literals - automatic interpolation of $variable tokens and $() subexpressions will occur unless explicitly escaped - ` is the escape character, and common sequences you'd expect in most C-like languages (`n, `t, `r, etc.) are natively recognized.

    Arbitrary expressions (like $dictionary.get_Item('some key')) will not be evaluated as-is, however.

    To get around that, we can either use the -f string format operator:

    $mymeters = [ordered]@{}
    "Here is item '500 HR': {0}" -f $mymeters['500 HR']
    

    -f should feel familiar if your used to Python3's f strings, but with a caveat - PowerShell's -f operator is a thin wrapper around String.Format(), and String.Format() only supports 0-based placeholders - '{0} {1}' -f 1,2 is valid but '{} {}' -f 1,2 is not.

    Another option is to wrap the expression in a $() sub-expression operator inside a double-quoted string literal:

    $mymeters = [ordered]@{}
    "Here is item '500 HR': $($mymeters['500 HR'])"
    

    Take note that dictionaries in PowerShell supports keyed index access with [] - just like Python :)


    Not unlike Python, PowerShell (and .NET in general) has powerful introspection capabilities as well.

    Dynamically discovering and iterating the properties of any object is as simple as referencing a special member named psobject:

    foreach($propertyMetadataEntry in $someObject.psobject.Properties){
        "Property: {0,-20} = {1}" -f $propertyMetadataEntry.Name,$propertyMetadataEntry.Value
    }
    

    Or in your case:

    $mymeters = $meter.METERS | ConvertFrom-Json
    foreach($meterReading in $mymeters.psobject.Properties){
        "Meter: {0,-20} = {1}" -f $meterReading.Name,$meterReading.Value
    
        # do whatever else you please with $meterReading here :)
    }
    

    This will work for any scalar object (like the object returned by ConvertFrom-Json or Invoke-RestMethod).

    For iterating over the entries in a dictionary, you need to explicitly call .GetEnumerator():

    $dictionary = [ordered]@{ A = 1; B = 2; C =3 }
    foreach($keyValuePair in $dictionary.GetEnumerator()){
        "Dictionary Entry: {0} = {1}" -f $keyValuePair.Key,$keyValuePair.Value
    }