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.
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? :-)