'Set property value on object loaded from json containing comments

When loading an object from a json file one can normally set the value on properties and write the file back out like so:

$manifest = (gc $manifestPath) | ConvertFrom-Json -AsHashtable
$manifest.name = "$($manifest.name)-sxs"
$manifest | ConvertTo-Json -depth 100 | Out-File $manifestPath -Encoding utf8NoBOM

But if the source json file contains comments, the object's properties can't be set:

// *******************************************************
// GENERATED FILE - DO NOT EDIT DIRECTLY
// *******************************************************
{
  "name": "PublishBuildArtifacts"
}

Running the code above throws an error:

$manifest

id                 : 1D341BB0-2106-458C-8422-D00BCEA6512A
name               : PublishBuildArtifacts
friendlyName       : ms-resource:loc.friendlyName
description        : ms-resource:loc.description
category           : Build
visibility         : {Build}
author             : Microsoft Corporation
version            : @{Major=0; Minor=1; Patch=71}
demands            : {}
inputs             : {@{name=CopyRoot; type=filePath; label=ms-resource:loc.input.label.CopyRoot; defaultValue=;
                     required=False; helpMarkDown=Root folder to apply copy patterns to.  Empty is the root of the
                     repo.}, @{name=Contents; type=multiLine; label=ms-resource:loc.input.label.Contents;
                     defaultValue=; required=True; helpMarkDown=File or folder paths to include as part of the
                     artifact.}, @{name=ArtifactName; type=string; label=ms-resource:loc.input.label.ArtifactName;
                     defaultValue=; required=True; helpMarkDown=The name of the artifact to create.},
                     @{name=ArtifactType; type=pickList; label=ms-resource:loc.input.label.ArtifactType;
                     defaultValue=; required=True; helpMarkDown=The name of the artifact to create.; options=}…}
instanceNameFormat : Publish Artifact: $(ArtifactName)
execution          : @{PowerShell=; Node=}

$manifest.name
PublishBuildArtifacts

$manifest.name = "sxs"
InvalidOperation: The property 'name' cannot be found on this object. Verify that the property exists and can be set.

When I strip the comments, I can overwrite the property.

Is there a way I can coax PowerShell to ignore the comments while loading the json file/convert the object and generate a writable object?



Solution 1:[1]

I'm not sure if this is intended, but seems like ConvertFrom-Json is treating the comments on the Json as $null when converting it to an object. This only happens if it's receiving an object[] from pipeline, with a string or multi-line string it works fine.

A simple way to demonstrate this using the exact same Json posted in the question:

$contentAsArray = Get-Content test.json | Where-Object {
    -not $_.StartsWith('/')
} | ConvertFrom-Json -AsHashtable

$contentAsArray['name'] = 'hello' # works

Here you can see the differences and the workaround, it is definitely recommended to use -Raw on Get-Content so you're passing a multi-line string to ConvertFrom-Json:

$contentAsString = Get-Content test.json -Raw | ConvertFrom-Json -AsHashtable
$contentAsArray = Get-Content test.json | ConvertFrom-Json -AsHashtable

$contentAsString.PSObject, $contentAsArray.PSObject | Select-Object TypeNames, BaseObject

TypeNames                                      BaseObject
---------                                      ----------
{System.Collections.Hashtable, System.Object}  {name}
{System.Object[], System.Array, System.Object} {$null, System.Collections.Hashtable}


$contentAsArray['name']      # null
$null -eq $contentAsArray[0] # True
$contentAsArray[1]['name']   # PublishBuildArtifacts
$contentAsArray[1]['name'] = 'hello'
$contentAsArray[1]['name']   # hello

Solution 2:[2]

Santiago Squarzon's helpful answer shows an effective solution and analyzes the symptom. Let me complement it with some background information.

tl;dr

  • You're seeing a variation of a known bug, still present as of PowerShell 7.2.1, where a blank line or a single-line comment as the first input object unexpectedly causes $null to be emitted (first) - see GitHub issue #12229.

  • Using -Raw with Get-Content isn't just a workaround, it is the right - and faster - thing to do when piping a file containing JSON to be parsed as whole to ConvertFrom-Json


For multiple input objects (strings), ConverFrom-Json has a(n unfortunate) heuristic built in that tries to infer whether the multiple strings represent either (a) the lines of a single JSON document or (b) separate, individual JSON documents, each on its own line, as follows:

  • If the first input string is valid JSON by itself, (b) is assumed, and an object representing the parsed JSON ([pscustomobject] or , with -AsHashtable, [hastable], or an array of either) is output for each input string.

  • Otherwise, (a) is assumed and all input strings are collected first, in a multi-line string, which is then parsed.

The aforementioned bug is that if the first string is an empty/blank line or a single-line comment[1] (applies to both // ... comments, which are invariably single-line, and /* ... */ if they happen to be single-line), (b) is (justifiably) assumed, but an extraneous $null is emitted before the remaining (non-blank, non-comment) lines are parsed and their object representation(s) are output.

As a result an array is invariably returned, whose first element is $null - which isn't obvious, but results in subtle changes in behavior, as you've experienced:

Notably, attempting to set a property on what is presumed to be a single object then fails, because the fact that an array is being accessed makes the property access an instance of member-access enumeration - implicitly applying property access to all elements of a collection rather than the collection itself - which only works for getting property values - and fails obscurely when setting is attempted - see this answer for details.

A simplified example:

# Sample output hashtable parsed from JSON
$manifest = @{ name = 'foo' }

# This is what you (justifiably) THOUGHT you were doing.
$manifest.name = 'bar' # OK

# Due to the bug, this is what you actually attempted.
($null, $manifest).name = 'bar' # !! FAILS - member-access enumeration doesn't support setting.

As noted above, the resulting error message - The property 'name' cannot be found on this object. ... - is unhelpful, as it doesn't indicate the true cause of the problem.

Improving it would be tricky, however, as the user's intent is inherently ambiguous: the property name may EITHER be a situationally unsuccessful attempt to reference a nonexistent property of the collection itself OR, as in this case, a fundamentally unsupported attempt at setting properties via member-access enumeration.

Conceivably, the following would help if the target object is a collection (enumerable) from PowerShell's perspective: The property 'name' cannot be found on this collection, and setting a collection's elements' properties by member-access enumeration isn't supported.


[1] Note that comments in JSON are supported in PowerShell (Core) v6+ only.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2