Powershell: ConvertFrom-Json Should Support Type Models

Created on 17 Feb 2018  路  9Comments  路  Source: PowerShell/PowerShell

(This would also be nice to apply to Invoke-RestMethod, but the change needs to start with ConvertFrom-Json, IMO)

I have been doing a lot of work recently in C# where I'm deserializing JSON and YAML. One thing that I really like about C# is the ability to provide models to deserialize into.

PSObjects are great and flexible, but when I'm working with a well defined endpoint in PowerShell, I find I'm often having to manually convert the deserialized JSON object to a strongly typed PowerShell class either by trying to type-cast the result (doesn't always work), through special constructors or factories, looping through the object, or manually deserializing with Newtonsoft.Json.Converter.

What I'd like is for this be rolled into ConfertFrom-Json. ideally it would work like this

class MySubClass {
    [String]$SubProperty1
    [String]$SubProperty2

    MySubClass() {}
}
class MyClass {
    [string]$Property1
    [MySubClass[]]$Property2

    MyClass(){}
}

$json = @'
{
    "Property1": "value1",
    "Property2": [
        {
            "SubProperty1": "value2",
            "SubProperty2": "value3"
        },
        {
            "SubProperty1": "value4",
            "SubProperty2": "value5"
        }
    ]
}
'@

$result = $json | ConvertFrom-Json -ModelType [MyClass]

It would essentially do this:

$result = [Newtonsoft.Json.JsonConvert]::DeserializeObject($json,[MyClass])

With the goal being this passes:

Describe "Result" {
    It "should be strongly typed" {
        $result | should -BeOfType [MyClass]
        $result.Property2 | should -BeOfType [MySubClass]
    }
    It "Should have correct values" {
        $result.Property1 | should -BeExactly "value1"
        $result.Property2[0].SubProperty1 | should -BeExactly "value2"
        $result.Property2[0].SubProperty2 | should -BeExactly "value3"
        $result.Property2[1].SubProperty1 | should -BeExactly "value4"
        $result.Property2[1].SubProperty2 | should -BeExactly "value5"
    }
}

result:

Describing Result
  [+] should be strongly typed 68ms
  [+] Should have correct values 19ms
Area-Cmdlets-Utility Issue-Enhancement

Most helpful comment

I'd be more inclined to go with something like -AsType perhaps?

$JsonData | ConvertFrom-Json -AsType MyType

All 9 comments

We already support casting from PSObject to a concrete type so try this:
$result = [MyClass] ($json | ConvertFrom-Json)
It should do what you want.

I run into performance problems when deserializing into PSObject.
400Mb takes 10 minuts and 10Gb, and lots of GC.

That seems to be done more efficiently when deserializing to a known type.

@BrucePay As I mentioned, type casting doesn't always work. If you have a constructor that accepts a PSObject then you cannot simply type case a convertfrom-json results because the constructor will be called.

Class MyClass {
    [string]$Property1 = 'default1'
    [string]$Property2 = 'default2'
    MyClass([PSObject]$Foo) {
        $this.Property1 = $Foo.Bar
        $this.Property2 = $Foo.Baz
    }
}

Class MyClass2 {
    [string]$Property1 = 'default1'
    [string]$Property2 = 'default2'
    MyClass() {}
}

$Json = @'
{
    "Property1": "json1",
    "Property2": "json2"
}
'@

Describe "Type casting a ConvertFrom-Json result" {
    It "Works when a PSObject constructor is present" {
        $result = [MyClass] ($json | ConvertFrom-Json)

        $result.Property1 | Should -BeExactly "json1"
        $result.Property2 | Should -BeExactly "json2"
    }

    It "Works when a PSObject constructor is not present" {
        $result = [MyClass2] ($json | ConvertFrom-Json)

        $result.Property1 | Should -BeExactly "json1"
        $result.Property2 | Should -BeExactly "json2"
    }
}

results:

Describing Type casting a ConvertFrom-Json result
  [-] Works when a PSObject constructor is present 39ms
    Expected strings to be the same, but they were different.
    Expected length: 5
    Actual length:   0
    Strings differ at index 0.
    Expected: 'json1'
    But was:  ''
    -----------^
    5:         $result.Property1 | Should -BeExactly "json1"
  [+] Works when a PSObject constructor is not present 26ms

I find I often need PSObject constructors because I'm working with outputs from cmdlets and such where the author has chosen PSObjects instead of classes for the output type.

This also breaks when a nested object type also has a PSObject constructor. However, Newtonsoft does work:

class MyNestedClass {
    [string]$NestedProperty = 'NestedDefault'
    MyNestedClass ([PSObject]$Foo) {
        $this.NestedProperty = $Foo.Bar
    }
}

Class MyClass {
    [String]$Property = 'Propertydeafult'
    [MyNestedClass[]]$NestedObjects
    MyClass () {}
}

$json = @'
{
    "Property": "value",
    "NestedObjects": [
        {
            "NestedProperty": "NestedValue1"
        },
        {
            "NestedProperty": "NestedValue2"
        }
    ]
}
'@

Describe "Type casting a ConvertFrom-Json result" {
    it "Works with nested objects that have a PSObject constructor" {
        $Result = [MyClass] ($json | ConvertFrom-Json)
        $Result.NestedObjects.Count | Should -Be 2
        $Result.NestedObjects[0].NestedProperty | Should -BeExactly "NestedValue1"
        $Result.NestedObjects[1].NestedProperty | Should -BeExactly "NestedValue2"
    }
}

Describe "Newtonsoft Class Model" {
    it "Works with nested objects that have a PSObject constructor" {
        $Result = [Newtonsoft.Json.JsonConvert]::DeserializeObject($json,[MyClass])
        $Result.NestedObjects.Count | Should -Be 2
        $Result.NestedObjects[0].NestedProperty | Should -BeExactly "NestedValue1"
        $Result.NestedObjects[1].NestedProperty | Should -BeExactly "NestedValue2"
    }
}

result:

Describing Type casting a ConvertFrom-Json result
  [-] Works with nested objects that have a PSObject constructor 52ms
    Expected strings to be the same, but they were different.
    Expected length: 12
    Actual length:   0
    Strings differ at index 0.
    Expected: 'NestedValue1'
    But was:  ''
    -----------^
    5:         $Result.NestedObjects[0].NestedProperty | Should -BeExactly "NestedValue1"

Describing Newtonsoft Class Model
  [+] Works with nested objects that have a PSObject constructor 25ms

The performance difference for me is considerable -- in my tests using convertfrom-json -ashashtable results in memory usage about 10x the size of the json but using a class and "[Newtonsoft.Json.JsonConvert]::DeserializeObject" results in about memory usage about 2x the size of the json string.

If ConvertFrom-Json could take an object declaration that would be awesome!

Seems fine to support an optional parameter. Perhaps just call it -TypeName?

I'd be more inclined to go with something like -AsType perhaps?

$JsonData | ConvertFrom-Json -AsType MyType

.Net Core 3.0 gets System.Text.Json https://github.com/dotnet/corefx/issues/33115
Plan there is to get new fast implementation. We could consider using it in the repo and create feedbacks to enhance System.Text.Json while it is under active development.

I second this:

$JsonData | ConvertFrom-Json -AsType MyType

I'll chime in here, too, that this use case would be a great feature add

Was this page helpful?
0 / 5 - 0 ratings