arrayspowershellenumerationpowershell-coreconvertfrom-json

Why is PowerShell applying the predicate of a `Where` to an empty list


If I run this in PowerShell, I expect to see the output 0 (zero):

Set-StrictMode -Version Latest

$x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
Write-Host $x.Count

Instead, I get this error:

The property 'name' cannot be found on this object. Verify that the     property exists and can be set.
At line:1 char:44
+     $x = "[]" | ConvertFrom-Json | Where { $_.name -eq "Baz" }
+                                            ~~~~~~~~~~~~~~~
+ CategoryInfo          : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException

If I put braces around "[]" | ConvertFrom-Json it becomes this:

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
Write-Host $y.Count

And then it "works".

What is wrong before introducing the parentheses?

To explain the quotes around "works" - setting strict mode Set-StrictMode -Version Latest indicates that I call .Count on a $null object. That is solved by wrapping in @():

$z = @(("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" })
Write-Host $z.Count

I find this quite dissatisfying, but it's an aside to the actual question.


Solution

  • Why is PowerShell applying the predicate of a Where to an empty list?

    Because ConvertFrom-Json tells Where-Object to not attempt to enumerate its output.

    Therefore, PowerShell attempts to access the name property on the empty array itself, much like if we were to do:

    $emptyArray = New-Object object[] 0
    $emptyArray.name
    

    When you enclose ConvertFrom-Json in parentheses, powershell interprets it as a separate pipeline that executes and ends before any output can be sent to Where-Object, and Where-Object can therefore not know that ConvertFrom-Json wanted it to treat the array as such.


    We can recreate this behavior in powershell by explicitly calling Write-Output with the -NoEnumerate switch parameter set:

    # create a function that outputs an empty array with -NoEnumerate
    function Convert-Stuff 
    {
      Write-Output @() -NoEnumerate
    }
    
    # Invoke with `Where-Object` as the downstream cmdlet in its pipeline
    Convert-Stuff | Where-Object {
      # this fails
      $_.nonexistingproperty = 'fail'
    }
    
    # Invoke in separate pipeline, pass result to `Where-Object` subsequently
    $stuff = Convert-Stuff
    $stuff | Where-Object { 
      # nothing happens
      $_.nonexistingproperty = 'meh'
    }
    

    Write-Output -NoEnumerate internally calls Cmdlet.WriteObject(arg, false), which in turn causes the runtime to not enumerate the arg value during parameter binding against the downstream cmdlet (in your case Where-Object)


    Why would this be desireable?

    In the specific context of parsing JSON, this behavior might indeed be desirable:

    $data = '[]', '[]', '[]', '[]' |ConvertFrom-Json
    

    Should I not expect exactly 5 objects from ConvertFrom-Json now that I passed 5 valid JSON documents to it? :-)