Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Powershell Add-Member - But without "Value" and "Count" elements in JSON

I am successfully adding a member to my JSON, but I end up with unwanted elements. What I am trying to add is the element inside "Value" that is appearing in the resulting JSON.

{
"Block1": value1,
"Block2": value2,
"Block3": []
}

Then doing the Add-Member cmdlet.

$objectFromJson |
  Add-Member -NotePropertyName "Block3" -NotePropertyValue $newblock -Force

I realize I do not have to do the -Force part, but in my working code, my JSON string is parsed to an object using ConvertFrom-Json and that part is effective for my purposes.

There are from 1 to N elements in the array stored in $newblock, to be serialized as array-valued property Block3.

Unfortunately, I end up with the following:

{
"Block1": value1,
"Block2": value2,
"Block3": [ { "value": { <elements of $newblock> }, "Count": <n> } ]
}

In the snippet above, <elements of $newblock> represent the JSON representation of the $newblock array elements, and <n> the count of elements in the array.

It is valid JSON, but not what I desire. Instead, I want the elements of $newblock to be direct elements of the Block3 array, without the extraneous wrapper object with the value and Count properties:

{
"Block1": value1,
"Block2": value2,
"Block3": [ <elements of $newblock> ]
}
like image 443
Taersious Avatar asked Oct 23 '25 14:10

Taersious


2 Answers

tl;dr

  • Your own solution avoids the original problem, and is arguably the better approach to begin with: Create the .Block3 property as an array via the original JSON (rather than later via Add-Member), and add elements to that array later with +=.

  • However, you could have fixed the original problem by simply (but obscurely) passing
    -NotePropertyValue $newblock.psobject.BaseObject instead of
    -NotePropertyValue $newblock, which removes the invisible [psobject] wrapper around the array stored in $newblock that caused the problem. Read on for an explanation.


What matters in the end is: the array stored in the .Block3 property must not have an invisible [psobject] wrapper, because in Windows PowerShell that causes the array to serialize to JSON wrapped in an extra object with "Count" and "values" properties.

The extra object in the JSON output stems from the presence of an obsolete ETS (extended-type system) property named .Count for arrays, which takes effect for [psobject]-wrapped arrays - see this answer for the gory details.

The problem no longer surfaces in PowerShell (Core) 7, because this ETS property has been removed there.

Add-Member was the wrong approach, since it adds a PSObject or PSCustomObject under the covers.

Actually, Add-Member by itself does not do that, because the -NotePropertyValue parameter is [object]-typed, not [psobject]-typed.

The array stored in your $newblock variable must already have been [psobject] wrapped:
$newblock -is [psobject] probably indicates $true for you, whereas a regular array does not (e.g., @(1, 2) -is [psobject] is $false)

For instance, an array returned from a cmdlet, as a whole will have an invisible [psobject] wrapper, notably when you use the New-Object cmdlet:
(New-Object string[] 2) -is [psobject] returns $true

See GitHub issue #5579 for a compilation of all scenarios in which this invisible extra [psobject] wrapper is added, which can cause other subtle behavioral differences as well, which still affect PowerShell (Core) 7 to this day (but, as stated, this particular issue has been fixed by the removal of the ETS property).


There are two general workarounds:

  • Session-wide:

    • Before calling ConvertTo-Json, run the following command, which removes the obsolete ETS property, after which arrays serialize as expected - whether [psobject]-wrapped or not:
      Remove-TypeData System.Array
  • For a given array variable:

    • Use .psobject.BaseObject to access the array's unwrapped, underlying .NET array; in your case: $newblock.psobject.BaseObject

Examples:

Session-wide workaround:

# The problem: Serialize a [psobject]-wrapped array (0, 0):
PS> ConvertTo-Json -InputObject (New-Object int[] 2)
# Note the extra object with the "count" (element count) and "value" property (elements)
{
    "value":  [
                  0,
                  0
              ],
    "Count":  2
}

# Remove the ETS definitions for System.Array
Remove-TypeData System.Array

# Rerun the command:
PS> ConvertTo-Json -InputObject (New-Object int[] 2)
# OK
[
    0,
    0
]

Workaround for a given array variable:

PS> $arr = New-Object int[] 2; ConvertTo-Json -InputObject $arr
# Note the extra object with the "count" (element count) and "value" property (elements)
{
    "value":  [
                  0,
                  0
              ],
    "Count":  2
}

# $arr.psobject.BaseObject bypasses the [psobject] wrapper
PS> ConvertTo-Json -InputObject $arr.psobject.BaseObject
# OK
[
    0,
    0
]
like image 82
mklement0 Avatar answered Oct 25 '25 06:10

mklement0


The solution to my issue was as follows (pseudo code):

$json = @"
{
"Block1": value1,
"Block2": value2,
"Block3": []
}
"@

$objFromJson = $json | ConvertFrom-Json

$listCount = ($newblock.ToCharArray() | Where-Object ($_ -eq ";" | Measure-Object).Count + 1

for($i = 0; $i -lt $listCount; $i++) {
  $newJson = @{ element1 = value1; element2 = value2; etc. }
  $objFromJson.Block3 += $newJson
}

$objFromJson | ConvertTo-Json

The part I had issues with was that Add-Member was the wrong approach, since it adds a PSObject or PSCustomObject under the covers.

Thanks to @mklement0 for pointing me to the root cause of my question.

like image 33
Taersious Avatar answered Oct 25 '25 07:10

Taersious