2

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
ledneb
  • 1,371
  • 1
  • 13
  • 25
  • First of all, `=` is not `-eq`! The second variation "works" because the `Where` is never evaluated (the collection is empty). Replace `"[]"` with `"[{}]"` for more insight. As to why the same doesn't apply to the first variation (i.e. why there is a pipeline, and the `Where` is applied to it) -- that's more interesting, and probably has to do with the subtleties of `ConvertFrom-Json`... – Jeroen Mostert Apr 08 '19 at 14:24
  • Voting to close as typo. The problem is with the `=`, nothing else. – Maximilian Burszley Apr 08 '19 at 14:36
  • @TheIncorrigible1 Typo fixed. Also pulled over setting strict mode, something I'd omitted. The problem persists. – ledneb Apr 08 '19 at 14:43
  • 1
    Strict mode causes an exception to be thrown if you try accessing a property that doesn't exist. If you want to avoid this, you should be using one of the other parameter sets such as: `| ? Name -eq Baz` – Maximilian Burszley Apr 08 '19 at 14:45
  • 1
    To clear up potential confusion over what this question is about: The question is why JSON input that becomes an _empty array_ in PowerShell via `ConvertFrom-Json` surprisingly still sends an object through the pipeline and executes the `Where-Object` script block, whereas it doesn't do that if you use an empty array directly (`@() | Where ...`). – mklement0 Apr 09 '19 at 14:36

2 Answers2

4

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

mklement0
  • 382,024
  • 64
  • 607
  • 775
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • 2
    Interesting! How does `ConvertFrom-Json` "tell" `Where-Object` not to attempt to enumerate it's output? – ledneb Apr 08 '19 at 14:53
  • Thanks for the updated answer! Why would this be desireable? Disallowing it seems to break pipelines, but I assume there's a good reason for why we _wouldn't_ want something to enumerate later in the pipeline? – ledneb Apr 08 '19 at 14:59
  • ...I suppose if I wanted to pipe a resulting json-decoded object in to something else it'd be a pain if it were decomposed in to multiple objects when it just happens to be an array. That'd result in multiple calls to the next stage of the pipeline where normally we'd expect one. Ok, thanks again Mathias, shout if I'm mis-thinking :) – ledneb Apr 08 '19 at 15:04
  • The surprising "un-PowerShell-like" default behavior of `ConvertFrom-Json` is discussed in https://github.com/PowerShell/PowerShell/issues/3424, along with potentially introduce a switch such as `-[No]Enumerate` to offer control over the enumeration behavior. – mklement0 Apr 08 '19 at 16:45
  • Quibble: `Where-Object` never has any knowledge of how the preceding pipeline segment produced its output - it simply operates on the inputs as provided: a single object that happens to be an array with a direct `ConvertFrom-Json` call, and the forced enumeration of that array when enclosed in `(...)`. – mklement0 Apr 08 '19 at 16:50
  • @mklement0 yeah, that's why I clarified that it "causes the **runtime** to *not* enumerate [...] during parameter binding against the downstream cmdlet" - but for the initial TLDR/ELI5 answer, I think it's a fair interpretation to say that "Where-Object doesn't know" :) – Mathias R. Jessen Apr 09 '19 at 14:16
  • But there's still the phrase _Because ConvertFrom-Json tells Where-Object to not attempt to enumerate its output_ at the top, which is misleading, because the former doesn't tell the latter anything. If anything, the former tells the _pipeline_ not to enumerate. – mklement0 Apr 09 '19 at 14:20
  • I agree completely, a more technically correct phrase would be *Because ConvertFrom-Json tells the runtime to not attempt to enumerate its output before deciding how to bind it to the input parameters exposed by Where-Object* and an even more correct version would be "When you hit enter, a signal is sent to the hardware scanner bus to which your keyboard is connected which in turn cause [436802 words later ...] and `consolhostv2.dll` finally calls the GFX renderer..." - I maintain that my _simplification_ is appropriate at the layer of abstraction we're discussion :) – Mathias R. Jessen Apr 09 '19 at 14:37
  • Ignoring the unwarranted sarcasm: To me, it's not a simplification, it's a false abstraction that actively suggests a relationship that doesn't exist, as evidenced by @ledneb inquiring about it. – mklement0 Apr 09 '19 at 22:48
2

With an empty array as direct pipeline input, nothing is sent through the pipeline, because the array is enumerated, and since there's nothing to enumerate - because an empty array has no elements - the Where (Where-Object) script block is never executed:

Set-StrictMode -Version Latest

# The empty array is enumerated, and since there's nothing to enumerate,
# the Where[-Object] script block is never invoked.
@() | Where { $_.name -eq "Baz" } 

By contrast, in PowerShell versions up to v6.x "[]" | ConvertFrom-Json produces an empty array as a single output object rather than having its (nonexistent) elements enumerated, because ConvertFrom-Json in these versions doesn't enumerate the elements of arrays it outputs; it is the equivalent of:

Set-StrictMode -Version Latest

# Empty array is sent as a single object through the pipeline.
# The Where script block is invoked once and sees $_ as that empty array.
# Since strict mode is in effect and arrays have no .name property
# an error occurs.
Write-Output -NoEnumerate @() | Where { $_.name -eq "Baz" }

ConvertFrom-Json's behavior is surprising in the context of PowerShell - cmdlets generally enumerate multiple outputs - but is defensible in the context of JSON parsing; after all, information would be lost if ConvertFrom-Json enumerated the empty array, given that you wouldn't then be able to distinguish that from empty JSON input ("" | ConvertFrom-Json).

The consensus was that both use cases are legitimate and that users should have a choice between the two behaviors - enumeration or not - by way of a switch (see this GitHub issue for the associated discussion).

Therefore, starting with PowerShell [Core] 7.0:

  • Enumeration is now performed by default.

  • An opt-in to the old behavior is available via the new -NoEnumerate switch.

In PowerShell 6.x-, if enumeration is desired, the - obscure - workaround is to force enumeration by simply enclosing the ConvertFrom-Json call in (...), the grouping operator (which converts it to an expression, and expressions always enumerate a command's output when used in the pipeline):

# (...) around the ConvertFrom-Json call forces enumeration of its output.
# The empty array has nothing to enumerate, so the Where script block is never invoked.
("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }

As for what you tried: your attempt to access the .Count property and your use of @(...):

$y = ("[]" | ConvertFrom-Json) | Where { $_.name -eq "Baz" }
$y.Count # Fails with Set-StrictMode -Version 2 or higher

With the ConvertFrom-Json call wrapped in (...), your overall command returns "nothing": loosely speaking, $null, but, more accurately, an "array-valued null", which is the [System.Management.Automation.Internal.AutomationNull]::Value singleton that indicates the absence of output from a command. (In most contexts, the latter is treated the same as $null, though notably not when used as pipeline input.)

[System.Management.Automation.Internal.AutomationNull]::Value doesn't have a .Count property, which is why with Set-StrictMode -Version 2 or higher in effect, you'll get a The property 'count' cannot be found on this object. error.

By wrapping the entire pipeline in @(...), the array subexpression operator, you ensure treatment of the output as an array, which, with array-valued null output, creates an empty array - which does have a .Count property.

Note that you should be able to call .Count on $null and [System.Management.Automation.Internal.AutomationNull]::Value, given that PowerShell adds a .Count property to every object, if not already present - including to scalars, in a commendable effort to unify the handling of collections and scalars.

That is, with Set-StrictMode set to -Off (the default) or to -Version 1 the following does work and - sensibly - returns 0:

# With Set-StrictMode set to -Off (the default) or -Version 1:

# $null sensibly has a count of 0.
PS> $null.Count
0

# So does the "array-valued null", [System.Management.Automation.Internal.AutomationNull]::Value 
# `. {}` is a simple way to produce it.
PS> (. {}).Count # `. {}` outputs 
0

That the above currently doesn't work with Set-StrictMode -Version 2 or higher (as of PowerShell [Core] 7.0), should be considered a bug, as reported in this GitHub issue (by Jeffrey Snover, no less).

mklement0
  • 382,024
  • 64
  • 607
  • 775