arrayspowershellapiinvoke-restmethodmember-enumeration

PowerShell Invoke-RestMethod skips a returned null value when navigating through the array values. Ideas?


I am calling an API that returns 3 values in an object "tags", which has values for "tags.name" and tags.results". The values below are what is returned. But as I try to navigate through the values, I can see that the "null" value, the {}, was not stored in the array. It just skipped it, but I need this value as it completely ruins the array.

Any idea how to correctly populate the array so it doesn't skip this {} value?

Response Values

values                      attributes                     
------                      ----------                     
{1605560351000 78.129448 3} @{machine_type=System.Object[]}
{}                                                         
{1605560354000 0 3}         @{machine_type=System.Object[]}

Resulting Array
0 : 1605560351000 78.129448 3
1 : 1605560354000 0 3
2 : 

The PowerShell nvoke and array code:

    $response = Invoke-RestMethod 'https://api' -Method 'POST' -Headers $headers -Body $body

    "Response Values"
    $response.tags.results
    ""
    "Resulting Array"
    "0 : " + $response.tags.results.values[0]
    "1 : " + $response.tags.results.values[1]
    "2 : " + $response.tags.results.values[2]

The returned JSON from Invoke-RestAPI. You can see where the returned value is null for the second node.

{
    "tags": [
        {
            "name": "StateComp1",
            "results": [
                {
                    "values": [
                        [
                            1605561152000,
                            75.436455,
                            3
                        ]
                    ],
                }
            ],
        },
        {
            "name": "StateComp2",
            "results": [
                {
                    "values": [],
                }
            ],
        },
        {
            "name": "StateComp3",
            "results": [
                {
                    "values": [
                        [
                            1605561469000,
                            0,
                            3
                        ]
                    ],
                }
            ],
        }
    ]
}


Solution

  • The problem is not specific to Invoke-RestMethod and is instead explained by the behavior of PowerShell's member-access enumeration feature:

    When you access $response.tags.results.values, the .values properties on the - multiple - .results properties (you're using nested member-access enumeration) are effectively enumerated as follows:

    $response.tags.results | ForEach-Object { $_.values }
    

    In doing so, empty arrays are effectively removed from the output, and you get only 2 output objects (that each contain the inner array of the nested ones in your case); you can verify that by applying (...).Count to the command above.

    The reason is that objects output by a script block ({ ... }) are sent to the pipeline, which by default enumerates objects that are collections (arrays).
    Since your 2nd .values value is an empty array (parsed from JSON [] and therefore not $null), there is nothing to enumerate and nothing is output, resulting in effective removal.

    Note that the above implies that collections of collections are flattened by member-access enumeration; for instance, if the property values are 3 2-element arrays, the pipeline-enumeration logic results in a single 6-element array rather than in a 3-element array containing 2-element arrays each.


    The workaround is to to use the ForEach() array method, which doesn't perform this stripping if you target the property by supplying its name as a string:

    $values = $response.tags.results.ForEach('values')
    
    @"
    Resulting Array"
    0 : $($values[0])
    1 : $($values[1])
    2 : $($values[2])
    "@
    

    Note that your non-empty .values properties are actually nested arrays; to shed the outer array, use $values[0][0], $values[1][0], and $values[2][0].

    Caveat: The workaround is only effective if you access the property by name string - .ForEach('values'); the seemingly equivalent script-block based command,
    .ForEach({ $_.values }) again removes empty arrays, like member-access enumeration and the ForEach-Object cmdlet; alternatively, however, you can work around that by wrapping the script-block output in an auxiliary, temporary single-element array that preserves the original arrays, using the unary form of , the array constructor operator:
    .ForEach({ , $_.values })
    You could also use the same technique with the - slower - ForEach-Object cmdlet.