2

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?

jessehouwing
  • 106,458
  • 22
  • 256
  • 341
  • 1
    Use the `-Raw` switch on `Get-Content` not sure if this is intended but it works that way. On the other hand, it's worth noting that the Json will not be compatible with Windows PowerShell – Santiago Squarzon Feb 07 '22 at 21:46

2 Answers2

2

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
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    Crazy stuff. Luckily `-Raw` did the trick. PowerShell keeps tripping my up with magic conversions and weird behavior. Usually when something magically turns into an array. I wish it would stop ding that. – jessehouwing Feb 08 '22 at 12:14
  • 1
    @jessehouwing _"magic conversions and weird behavior"_ sounds a lot like PowerShell still one grows to love it lol. Happy it worked out for you. – Santiago Squarzon Feb 08 '22 at 12:22
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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Thanks, this is indeed helpful. What also would have helped is when PowerShell would clearly dump the array to the console instead of the json object. As you can see dumping `$manifest`akes no mention of this being an array or the fest element being null. Though it may be that that's why it renders a blank line. Even in the debugger under Vscode, this isn't obvious. – jessehouwing Feb 09 '22 at 18:51
  • Glad to hear it, @jessehouwing. The problem with detecting this is that PowerShell's output-formatting system generally doesn't visualize `$null` - neither by itself, nor as part of arrays; e.g., `$null, 'foo'` renders the same as `'foo'`. Sadly, `Measure-Object` too ignores `$null`s (see https://github.com/PowerShell/PowerShell/issues/10905). Your best (obscure) bet is to use `Format-Custom -InputObject ($null, 'foo')` (note: use `-InputObject`, not pipeline input), which renders a `$null` as an empty line. (And, of course, `$var.GetType()` will tell you whether something is an array). – mklement0 Feb 09 '22 at 19:05
  • Would setting the type of the variable have surfaced the issue? – jessehouwing Feb 09 '22 at 19:07
  • 1
    @jessehouwing, yes, type-constraining the variable would have surfaced an error: `[hashtable] $manifest = ...` without `-Raw` reports `Cannot convert the "System.Object[]" value of type "System.Object[]" to type "System.Collections.Hashtable".` – mklement0 Feb 09 '22 at 19:10
  • 1
    @jessehouwing, however, it wouldn't help if you omitted `-AsHashTable` and type-constrained with `[pscustomobject]`, because casting an array to `[pscustomobject]` simply wraps the whole array in a (mostly) invisible `[psobject]` wrapper (`[psobject]` is the same as `[pscustomobject]`, sadly, and use of `[pscustomobject]` outside of a custom-object _literal_ is virtually pointless). – mklement0 Feb 09 '22 at 19:16