7

I want to set value of nested object property using PowerShell. When you are trying to set the value of the first level properties, it's quiet simple:

$propertyName = "someProperty"
$obj.$propertyName = "someValue"  # ← It works

For nested properties, it doesn't work:

$propertyName = "someProperty.someNestedProperty"
$obj.$propertyName = "someValue"  # ← It doesn't work and raises an error.

How to set value of nested object property by name of property using PowerShell?

MCVE

For those who want to reproduce the problem, here is a simple example:

$Obj= ConvertFrom-Json '{ "A": "x", "B": {"C": "y"} }'
# Or simply create the object:
# $Obj= @{ A = "x"; B = @{C = "y"} }
$Key = "B.C"
$Value = "Some Value"
$Obj.$Key = $Value

Run the command and you will receive an error:

"The property 'B.C' cannot be found on this object. Verify that the property exists and can be set."

Note: The code supports any level of nesting.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • It has no way of knowing that you are asking for a nested property. I am trying to find what I think this is a dupe target for. You need to build logic to support nested properties in single strings. This would work but is not what you want `$json.$propertyName.$nestedPropertyName.` since it only would satisfy that one use case. A recursive function is needed iirc – Matt Sep 27 '17 at 15:51
  • I think this is what I was thinking of https://stackoverflow.com/questions/45174708/powershell-turn-period-delimited-string-into-object-properties/45175340#45175340 – Matt Sep 27 '17 at 15:54
  • @Matt Thanks for the comment. I'me aware of `$json.$propertyName.$nestedPropertyName`. But it's not based on property name and doesn't satisfy the requirement of resolving property by name at run-time. Also about the linked post, it's *Get*, while I'm looking for *Set*. – Reza Aghaei Sep 27 '17 at 15:56
  • Yeah...I know... Have a look at the linked answer though and see if that gets you in a better direction. – Matt Sep 27 '17 at 15:57

3 Answers3

15

I created SetValue and GetValue functions to let you get and set a nested property of an object (including a json object) dynamically by name and they work perfectly!

They are recursive functions which resolve the complex property and get the nested property step by step by splitting the nested property name.

GetValue and SetValue of Nested properties by Name

# Functions
function GetValue($object, $key)
{
    $p1,$p2 = $key.Split(".")
    if($p2) { return GetValue -object $object.$p1 -key $p2 }
    else { return $object.$p1 }
}
function SetValue($object, $key, $Value)
{
    $p1,$p2 = $key.Split(".")
    if($p2) { SetValue -object $object.$p1 -key $p2 -Value $Value }
    else { $object.$p1 = $Value }
}

Example

In the following example, I set B.C dynamically using SetValue and get its value by name using the GetValue function:

# Example
$Obj = ConvertFrom-Json '{ "A": "x", "B": {"C": "y"} }'
# Or simply create the object:
# $Obj = @{ A = "x"; B = @{C = "y"} }
$Key = "B.C"
$Value = "Changed Dynamically!"
SetValue -object $Obj -key $Key -Value $Value
GetValue -object $Obj -key $Key
mklement0
  • 382,024
  • 64
  • 607
  • 775
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • 1
    I would upvote this +20 if I could. This helped by set connectionstrings in an appsettings.json file for a .net project. – longday Jul 20 '18 at 20:56
4

Your own solutions are effective, but do not support indexed access as part of the nested property-access path (e.g., B[1].C)

A simple alternative is to use Invoke-Expression (iex). While it should generally be avoided, there are exceptional cases where it offers the simplest solution, and this is one of them:

Assuming you fully control or implicitly trust the property-access string:

$obj = ConvertFrom-Json '{ "A": "x", "B": [ {"C": "y"}, { "C": "z"} ] }'

$propPath = 'B[1].C'

# GET
Invoke-Expression "`$obj.$propPath" # -> 'z'

# SET
$value = 'Some Value'
Invoke-Expression "`$obj.$propPath = `$value" 

If you don't trust the input, you can avoid unwanted injection of commands as follows:[1]

$safePropPath = $propPath -replace '`|\$', '`$&'
Invoke-Expression "`$obj.$safePropPath"
# ...

For a convenience function / ETS method that safely packages the functionality above, see this answer.


[1] The regex-based -replace operation ensures that any $ characters in the string are escaped as `$, to prevent them Invoke-Expression from treating them as variable references or subexpressions; similarly, preexisting ` instances are escaped by doubling them.

mklement0
  • 382,024
  • 64
  • 607
  • 775
3

May I propose an upgrade to Reza's solution. With this solution, you can have many level of nested properties.

function GetValue($object, [string[]]$keys)
{
    $propertyName = $keys[0]
    if($keys.count.Equals(1)){
        return $object.$propertyName
    }
    else { 
        return GetValue -object $object.$propertyName -key ($keys | Select-Object -Skip 1)
    }
}


function SetValue($object, [string[]]$keys, $value)
{
    $propertyName = $keys[0]
    if($keys.count.Equals(1)) {
        $object.$propertyName = $value
    }
    else { 
        SetValue -object $object.$propertyName -key ($keys | Select-Object -Skip 1) -value $value
    }
}

Usage

$Obj = ConvertFrom-Json '{ "A": "x", "B": {"C": {"D" : "y"}} }'
SetValue $Obj -key "B.C.D".Split(".") -value "z"
GetValue $Obj -key "B.C.D".Split(".")
IV V
  • 71
  • 3