1

I want to create an array that looks like this

{
  "requestType": "addUsersToGroups",
  "orgToken": "organization_api_key",
  "assignedUsers" :
  [
    [{"name":"group_name"},[{"email":"assigned_user_email"}]]
  ]
}

But, an extra [] in assignedUsers is what I'm not able to get. I'm unable to get a list inside list for assignedUsers. All I'm getting is a list.

This is what I tried:

$assignedUsers = (
                (
                        @{"name" = "group_members_$productName"},
                        (
                                @{"email" = $user}
                        )
                )
        )
        $body= @{
                "requestType" = "addUsersToGroups"
                "productName" = $productName
                "userKey" = $userKey
                "orgToken" = $orgToken
                "assignedUsers" = $assignedUsers
        } | ConvertTo-Json

        Write-Output $body

But, the output is

{
    "requestType":  "addUsersToGroups",
    "assignedUsers":  [
                          {
                              "name":  "group_members_NewProduct"
                          },
                          {
                              "email":  "my_Email"
                          }
                      ],
    "userKey":  "userKey",
    "orgToken":  "orgToken",
    "productName":  "NewProduct"
}
7_R3X
  • 3,904
  • 4
  • 25
  • 43

2 Answers2

2

Arrays are not created by () or @(). They are created by , – the comma operator(*) see below.

As soon as you either separate multiple items with a comma, or prefix a single item with a comma, an array is the result:

$a = 1,2,3   # -> [1,2,3]
$a = ,1      # -> [1]

Note that when the , is in the front, it will always (!) produce a single-item array:

$a = ,1,2,3  # -> [[1],2,3]
$a = ,,1     # -> [[1]]

We can use this behavior to create nested single-item arrays:

$assignedUsers = ,(
    @{"name" = "group_name"},
    ,@{"email" = "assigned_user_email"}
)

$body = @{
    "assignedUsers" = $assignedUsers
}

$body | ConvertTo-Json -Depth 5 -Compress
# -> {"assignedUsers":[[{"name":"group_name"},[{"email":"assigned_user_email"}]]]}

Note that I've done it again for the email. Also there are no @() because they are not needed.


(*) In fact, @() also creates an array. @(1) will be a single-item array. But in @(1,2), what actually creates the array is the comma, whether you additionally wrap it in @() or not makes no difference, and it's in fact unnecessary.

However, @() processes its contents. If an item is iterable, @() will iterate it and merge the results into a new array (that's called "unrolling"):

$a = 1,2,3
$b = 4,5,6
$c = @(
    $a          # iterable
    $b          # iterable
)               # -> [1,2,3,4,5,6]

That's also why you cannot nest it to create nested arrays. @(@(1,2,3)) is still only [1,2,3], because first the inner @() gets an array, iterates it, and outputs it. Then the outer @() gets that array, iterates it, you get the picture. These are equivalent:

@{"prop" = 1,2,3}       | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}
@{"prop" = (1,2,3)}     | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}
@{"prop" = @(1,2,3)}    | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}
@{"prop" = @(@(1,2,3))} | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}

The unary , on the other hand does nothing of that sort. It simply takes whatever thing there is to the right of it and wraps it in an array.

@{"prop" = 1,2,3}       | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}
@{"prop" = ,1,2,3)}     | ConvertTo-Json -Compress  # -> {"prop":[[1],2,3]}
@{"prop" = ,(1,2,3)}    | ConvertTo-Json -Compress  # -> {"prop":[[1,2,3]]}
@{"prop" = ,@(1,2,3)}   | ConvertTo-Json -Compress  # -> {"prop":[[1,2,3]]}

$items = 1,2,3
@{"prop" = $items}      | ConvertTo-Json -Compress  # -> {"prop":[1,2,3]}
@{"prop" = ,$items}     | ConvertTo-Json -Compress  # -> {"prop":[[1,2,3]]}

So using @() is optional, you don't really need it to create arrays. You can use it when you require the "unrolling" behavior, or for the sake of source code formatting, because commas are optional inside of it when you use line-breaks to separate the elements. The results of

@(
  "a very long thing"
  "another very long thing"
  "yet another very long thing"
)

and

"a very long thing","another very long thing","yet another very long thing"

are equivalent, but the former is easier to read. More in-depth information on how @() works is over here.

Tomalak
  • 332,285
  • 67
  • 532
  • 628
  • 1
    Although your code example is correct, your explanation is not entirely correct. The [array subexpression operator](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-6#array-subexpression-operator--) (`@()`) *is* definitely used for creating (empty or single item) arrays and the [unary comma operator](https://docs.microsoft.com/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-6#comma-operator-) (`'`) is used to create a array with a single item and prevents is to unroll if the embedded item is an array – iRon Sep 02 '19 at 15:58
  • 1
    Hm... The entire array handling in Powershell is somewhere between byzantine and broken to me, I've simply come to accept it as weird. I'll try to improve my explanation. – Tomalak Sep 02 '19 at 16:27
  • @iRon , Tomalak : Thanks to both of you. I've been stuck with this problem for hours! Why and how did microsoft engineers come up with an idea saying, "lets make Windows automation tasks easier by inventing Powershell but hold on! Lets also not make it too easy!" – 7_R3X Sep 03 '19 at 05:48
  • It's at the very least counter-intuitive (and syntactically wonky) and Microsoft does not exactly fall over themselves explaining it properly in the official docs. I'm still being bitten by this on a regular basis. – Tomalak Sep 03 '19 at 06:00
  • I guess the whole thought behind this is to make properties of singletons easy accessible, e.g.: `(... | Select-Object First 1).MyProp` (related to this: [Property Enumeration](https://stackoverflow.com/a/50118339/1701026)). Saying this, @Tomalak (`+1` for the investigation/explanation), I think you still underestimating the use of the array subexpression operator (`@()`): It is especially useful for iterating through an output with an ***unknown*** number items (where a singleton is usually unrolled). In other words, `Foreach ($i in @($Unknown))` will make sure that `$Unknown` is enumerable. – iRon Sep 03 '19 at 07:45
  • 1
    Yes, that's discussed in the post I've linked to. I'm not saying `@()` is useless, I'm saying it is not (and should not be used or understood as) the primary way to construct arrays in PowerShell, because that seems to be a hugely common misconception. It has its place as the "make sure this is iterable" and "concat multiple iterables" operator. – Tomalak Sep 03 '19 at 08:15
1

Try building your $assignedusers and $body like this:

$orgToken    = "organization_api_key"
$userKey     = "userkey"
$productName = "NewProduct"

$assignedUsers = ,@(
    @{"name" = "group_name"},
    @(@{"email" = "assigned_user_email"})
)

$body = [ordered]@{
    "requestType"   = "addUsersToGroups"
    "orgToken"      = $orgToken
    "productName"   = $productName
    "userKey"       = $userKey
    "assignedUsers" = $assignedUsers
}

$body | ConvertTo-Json -Depth 5

Because the ConvertTo-Json cmdlet creates valid, but rather ugly formatted json, you can make use of the Format-Json function I posted here some time ago.

With that function in place, just change the last line in the code to

$body | ConvertTo-Json -Depth 5 | Format-Json

to get this result:

{
  "requestType": "addUsersToGroups",
  "orgToken": "organization_api_key",
  "productName": "NewProduct",
  "userKey": "userkey",
  "assignedUsers": [
    [
      {
        "name": "group_name"
      },
      [
        {
          "email": "assigned_user_email"
        }
      ]
    ]
  ]
}
Theo
  • 57,719
  • 8
  • 24
  • 41
  • The OP wants a doubly-nested array in `assignedUsers`, i.e. each user is an array, and there can be more than one user. – Tomalak Sep 02 '19 at 14:32
  • @Tomalak You're right, I have misread that part. Edited my answer now to double-nest the assignedusers – Theo Sep 03 '19 at 10:29
  • I like the formatter function, the default JSON format of PowerShell is indeed very ugly. I'm not sure if the regex approach is trustworthy, though - I would prefer a "minimal parser" approach such as the one as [shown here](https://stackoverflow.com/a/23828858/18771). Leaving regex out of the picture probably also is beneficial for performance. – Tomalak Sep 03 '19 at 11:03
  • @Tomalak I've just tried that one, but that function still leaves a lot of redundant whitespace between the keys and values. It also adds empty newlines after every line. (Could be my amaturistic way of using C# code in PowerShell though..) – Theo Sep 03 '19 at 11:44
  • No, that's actually because of a bug in the code. I've already fixed the answer I linked to, try again! – Tomalak Sep 03 '19 at 11:50
  • I would probably parameterize the indentation depth and use spaces instead of tabs for indentation, and also keep your choice of "Prettify vs. Minify" - and you can remove the part that parses and re-serializes the JSON in the "Minify" case. You're in control of all the whitespace output inside the loop, implementing "minify" is super easy. – Tomalak Sep 03 '19 at 11:54
  • @Tomalak I tried again using your fix to the C# code in the link and now it is indeed MUCH better. I'll keep that one as alternative for sure! – Theo Sep 03 '19 at 12:03
  • Hm, would be interesting to see some comparative measurements on large JSON inputs. My hunch is thatr the loop is significantly faster, but it could be that Powershell overhead eats up the advantage. – Tomalak Sep 03 '19 at 12:16