5

I'm trying to create a JSON array using:

$bodyObject = @(
    @{    
        'Username' = '[email protected]'        
    }
)

$body = $bodyObject | ConvertTo-Json

But the $body object doesn't contain the square brackets:

{
    "Username":  "[email protected]"
}

If I add another element to the array, the code works perfectly:

$bodyObject = @(
    @{    
        'Username' = '[email protected]'        
    },
    @{    
        'Username' = '[email protected]'        
    }
)

$body = $bodyObject | ConvertTo-Json
<# Output:
[
    {
        "Username":  "[email protected]"
    },
    {
        "Username":  "[email protected]"
    }
]
#>

How can I get one element arrays to generate JSON containing the square brackets?

2
  • Change $bodyObject | ConvertTo-Json to ConvertTo-Json $bodyObject. Commented Jun 14, 2023 at 23:51
  • 1
    @SantiagoSquarzon That worked, thanks! Why not write it as an answer so I can accept it? Commented Jun 14, 2023 at 23:52

2 Answers 2

4

The simplest way to do it is to pass the array positionally instead of through the pipeline:

$body = ConvertTo-Json $bodyObject

Reason why you don't see the array in the first example is because the pipeline enumerates.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for that. Btw I tried to Base64 decode the text on your profile but it came back with random chars. Is it Base64 encoded text?
@DavidKlempfner its gzip b64 ;)
4

To complement Santiago's helpful answer:

  • The enumeration behavior of PowerShell's pipeline means that a receiving command fundamentally cannot tell the difference between pipeline input that was provided as (a) a single input object or (b) as a single-element array.

    • That is, the following two commands both send a single [int] instance through the pipeline:

      • (42 | ForEach-Object GetType).Name -> Int32
      • (@(42) | ForEach-Object GetType).Name -> Int32
  • By contrast, when passing input as an argument, the target command can make such a distinction - if designed to do so - and ConvertTo-Json does.

    • However, it is rare for cmdlets to make this distinction - see GitHub issue #4242 for a discussion.

As an alternative to passing input by argument, PowerShell (Core) 7+ introduced the
-AsArray switch
, which requests that even a single input object (which may have been a single-element array originally) be treated as an array in its JSON representation.

# PS v7+ only; ditto for @(42) as input.
42 | ConvertTo-Json -AsArray -Compress # -> '[42]'

As iRon points out, you can achieve the same outcome by ensuring that a given array - even if it contains just one element - is sent through the pipeline as a whole, which also works in Windows PowerShell.

  • Note: While with ConvertTo-Json it's much simpler to pass an array as an argument to ConvertTo-Json, as shown in Santiago's answer, the techniques below may be of interest for commands that do not support passing array-valued arguments or support pipeline input only.
# Works in Windows PowerShell too.
# The unary form of the "," operator ensures that the array
# is sent *as a whole* through the pipeline.
, @(42) | ConvertTo-Json -Compress # -> '[42]'

The unary form of ,, the array constructor ("comma") operator constructs what acts as a transient, auxiliary array here:

  • Its one and only element is the input array.
  • When the pipeline enumerates this array, its one and only element - the array of interest - is sent as a whole through the pipeline.

There's a less obscure - but less efficient - alternative, using Write-Output with its -NoEnumerate switch:

# Works in Windows PowerShell too.
# -NoEnumerate prevents enumeration of the input array 
# and sends it through the pipeline as a whole.
Write-Output -NoEnumerate @(42) | ConvertTo-Json -Compress # -> '[42]'

Note:

  • While the result is the same as with the v7+ -AsArray switch, the mechanism is different:

  • With the auxiliary-array / non-enumeration technique, ConvertTo-Json truly receives an array as its one and only input object.

  • With the v7+ -AsArray switch, when it receives a scalar (non-array) as its only input object, it still treats it as an array.

  • If multiple input objects are received, -AsArray is a no-op, because even without this switch a JSON array must of necessity be output, given that ConvertTo-Json alway collects its input up front and then outputs a single JSON document for it.

  • Do not use -AsArray in combination with an argument (as opposed to pipeline input), as that will result in a nested JSON array, at least as of this writing (PowerShell 7.3.4):

    ConvertTo-Json -AsArray -Compress @(42) # !! -> '[[42]]'   
    

The design rationale behind PowerShell's enumeration behavior:

PowerShell is built around pipelines: data conduits through which objects stream, one object at a time.[1]

PowerShell commands output to the pipeline by default, and any command can write any number of objects, including none - and that number isn't known in advance, because it can vary depending on arguments and external state.

  • E.g., Get-ChildItem *.txt can situationally emit none, 1, or multiple objects.

Since the pipeline is just a stream of objects of unspecified count, there is no concept of an array in the pipeline itself, neither on input nor on output:

  • On input, arrays (and most enumerables)[2] are enumerated, i.e. the elements are sent one by one to the pipeline. Therefore, there is no difference between sending a scalar (single object) and sending a single-element array through the pipeline, as demonstrated above.

  • On output, multiple objects are simply output one at a time (though it is possible, but rare, to send an array (or other list-like type) as a whole, but it is then itself just another, single output object in the pipeline).

    • It is only when you collect a pipeline's output that arrays come into play, of necessity:

      • A single output object needs no container, and can just be received as itself.

      • Multiple objects need a container, and PowerShell automatically creates a System.Object[] array to collect the output objects in.


[1] You can introduce buffering of multiple objects with the common -OutBuffer parameter, but the next command in a pipeline still receives the buffered objects one by one.

[2] For details, see the bottom section of this answer.

2 Comments

Is there any reason why the creators of Powershell throught it'd be good to have (42 | ForEach-Object GetType).Name -> Int32 and (@(42) | ForEach-Object GetType).Name -> Int32 do the same thing? No other language I know of acts like this, it's just confusing and unintuitive.
@DavidKlempfner, as for the design rationale: please see the bottom section I've just added to the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.