Why does powershell give different result in one-liner than two-liner when converting JSON?

Note: The problem still exists as of Windows PowerShell v5.1, but PowerShell Core (v6+) is not affected.

The existing answers provide an effective workaround – enclosing $orig | ConvertFrom-JSON in (...) – but do not explain the problem correctly; also, the workaround cannot be used in all situations.


As for why use of an intermediate variable did not exhibit the problem:

The in-pipeline distinction between emitting an array’s elements one by one vs. the array as a whole (as a single object) is nullified if you collect the output in a variable; e.g., $a = 1, 2 is effectively equivalent to $a = Write-Output -NoEnumerate 1, 2, even though the latter originally emits array 1, 2 as a single object; however, the distinction matters if further pipeline segments process the objects – see below.


The problematic behavior is a combination of two factors:

  • ConvertFrom-Json deviates from normal output behavior by sending arrays as single objects through the pipeline. That is, with a JSON string representing an array, ConvertFrom-Json sends the resulting array of objects as a single object through the pipeline.

    • You can verify ConvertFrom-Json‘s surprising behavior as follows:

        PS> '[ "one", "two" ]' | ConvertFrom-Json | Get-Member
      
        TypeName: System.Object[]  # !! should be: System.String
        ...
      
    • If ConvertFrom-Json passed its output through the pipeline one by one – as cmdlets normally do – Get-Member would instead return the (distinct) types of the items in the collection, which is [System.String] in this case.

      • Enclosing a command in (...) forces enumeration of its output, which is why ($orig | ConvertFrom-Json) | ConvertTo-Json is an effective workaround.
    • Whether this behavior – which is still present in PowerShell Core too – should be changed is being debated in this GitHub issue.

  • The System.Array type – the base type for all arrays – has a .Count property defined for it via PowerShell’s ETS (Extended Type System – see Get-Help about_Types.ps1xml), which causes ConvertTo-Json to include that property in the JSON string it creates, with the array elements included in a sibling value property.

    • This happens only when ConvertTo-Json sees an array as a whole as an input object, as produced by ConvertFrom-Json in this case; e.g., , (1, 2) | ConvertTo-Json surfaces the problem (a nested array whose inner array is sent as a single object), but
      1, 2 | ConvertTo-Json does not (the array elements are sent individually).

    • This ETS-supplied .Count property was effectively obsoleted in PSv3, when arrays implicitly gained a .Count property due to PowerShell now surfacing explicitly implemented interface members as well, which surfaced the ICollection.Count property (additionally, all objects were given an implicit .Count property in an effort to unify the handling of scalars and collections).

    • Sensibly, this ETS property has therefore been removed in PowerShell Core, but is still present in Windows PowerShell v5.1 – see below for a workaround.


Workaround (not needed in PowerShell Core)

Tip of the hat, as many times before, to PetSerAl.

Note: This workaround is PSv3+ by definition, because the Convert*-Json cmdlets were only introduced in v3.

Given that the ETS-supplied .Count property is (a) the cause of the problem and (b) effectively obsolete in PSv3+, the solution is to simply remove it before calling ConvertTo-Json – it is sufficient to do this once in a session, and it should not affect other commands:

 Remove-TypeData System.Array # Remove the redundant ETS-supplied .Count property
 

With that, the extraneous .Count and .value properties should have disappeared:

 PS> '[ "one", "two" ]' | ConvertFrom-Json | ConvertTo-Json
 [
   "one",
   "two"
 ]

The above workaround also fixes the problem for array-valued properties; e.g.:

PS> '' | Select-Object @{ n='prop'; e={ @( 1, 2 ) } } | ConvertTo-Json
{
    "prop":  [
                 1,
                 2
             ]
}

Without the workaround, the value of "prop" would include the extraneous .Count and .value properties as well.

Leave a Comment