(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
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
Most helpful comment
I'd be more inclined to go with something like
-AsType
perhaps?