NapkinBob
NapkinBob

Reputation: 724

Deserializing JSON with multiple elements of the same type with different name

I am attempting to deserialize some json into a custom object in VB.net 4.5

The Json structure can have multiple elements with different names, and the names may not be the same so I can't create a static class for each possible name, because I don't know what they'll be. the structure of all the child elements is the same. I cannot control the structure of the json, it comes to me from another application.

Here's a simplified version of the JSON:

{
"Item1": {
    "config": {
        "PortInfo" : {
            "baud": 19200,
            "dataLength": 8,
            "flowControl": 0,
            "parity": 0  
        },
        "Custom": {
            "116": 1,
            "117": 2,
            "129": 85,
            "123": 1,
            "124": 0,
            "125": 2
        }
    }
},
"Item3": {
    "config": {
        "PortInfo" : {
            "baud": 19200,
            "dataLength": 8,
            "flowControl": 0,
            "parity": 0  
        },
        "Custom": {
            "116": 1,
            "117": 2,
            "129": 85,
            "123": 1,
            "124": 0,
            "125": 2
        }
    }
},
"Item4": {
    "config": {
        "PortInfo" : {
            "baud": 19200,
            "dataLength": 8,
            "flowControl": 0,
            "parity": 0  
        },
        "Custom": {
            "116": 1,
            "117": 2,
            "129": 85,
            "123": 1,
            "124": 0,
            "125": 2
        }
    }
},
"Errors": [
    [
        "com2",
        "port busy"
      ],
      [
        "com4",
        "port busy"
      ]
],
"timestamp": "2023-11-13 15:31:29"
}

I created the following classes to hold the data:

Public Class Rootobject
    Public Property Item As ItemObj
    Public Property Errors As String()
    Public Property timestamp As String
End Class

Public Class ItemObj
    Public Property config As Config
End Class

Public Class Config
    Public Property PortInfo As Portinfo
    Public Property Custom As Dictionary(Of String, Integer)
End Class

Public Class Portinfo
    Public Property baud As Integer
    Public Property dataLength As Integer
    Public Property flowControl As Integer
    Public Property parity As Integer
End Class

I can't figure out how to deserialize this JSON into my object, resulting in

The closest I got was if I remove the errors and timestamp from the JSON (so it's only the items, then I can populate the object with result = Newtonsoft.Json.JsonConvert.DeserializeObject(Of Dictionary(Of String, ItemObj))(Jsonstring) (Based on the answer here). If I leave the Errors and timestamp elements, then I get an exception saying:

Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'PrinterAudit.Rootobject' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.

If all of the items were their own array or element, then it would be easy, but the fact that there's the Errors and the timestamp elements at the same level as the Items, it's really throwing me off.

EDIT

Using dbc's solution, I've attempted to convert to VB resulting in the following converter:

Imports System.Runtime.CompilerServices
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Linq
Imports Newtonsoft.Json.Serialization

<AttributeUsage((AttributeTargets.Field Or AttributeTargets.Property), AllowMultiple:=False)>
Public Class JsonTypedExtensionDataAttribute
    Inherits Attribute
End Class
Public Class TypedExtensionDataConverter(Of TObject)
    Inherits JsonConverter

    Public Overrides Function CanConvert(ByVal objectType As Type) As Boolean
        Return GetType(TObject).IsAssignableFrom(objectType)
    End Function

    Private Function GetExtensionJsonProperty(ByVal contract As JsonObjectContract) As JsonProperty
        Try
            Return contract.Properties.Where(Function(p) p.AttributeProvider.GetAttributes(GetType(JsonTypedExtensionDataAttribute), False).Any()).[Single]()

        Catch ex As InvalidOperationException
            Throw New JsonSerializationException(String.Format("Exactly one property with JsonTypedExtensionDataAttribute is required for type {0}", contract.UnderlyingType), ex)
        End Try

    End Function

    Public Overrides Function ReadJson(ByVal reader As JsonReader, ByVal objectType As Type, ByVal existingValue As Object, ByVal serializer As JsonSerializer) As Object
        If (reader.TokenType = JsonToken.Null) Then
            Return Nothing
        End If

        Dim jObj = JObject.Load(reader)
        Dim contract = CType(serializer.ContractResolver.ResolveContract(objectType), JsonObjectContract)
        Dim extensionJsonProperty = Me.GetExtensionJsonProperty(contract)
        Dim extensionJProperty = CType(Nothing, JProperty)
        Dim i As Integer = (jObj.Count - 1)
        Do While (i >= 0)
            Dim prop = CType(jObj.AsList(i), JProperty)
            If contract.Properties.GetClosestMatchProperty(prop.Name) Is Nothing Then
                If (extensionJProperty Is Nothing) Then
                    extensionJProperty = New JProperty(extensionJsonProperty.PropertyName, New JObject)
                    jObj.Add(extensionJProperty)
                End If

                CType(extensionJProperty.Value, JObject).Add(prop.RemoveFromLowestPossibleParent)
            End If

            i = (i - 1)
        Loop

        Dim value = If(existingValue, contract.DefaultCreator())
        Using subReader = jObj.CreateReader
            serializer.Populate(subReader, value)
        End Using
        Return value
    End Function

    Public Overrides Sub WriteJson(ByVal writer As JsonWriter, ByVal value As Object, ByVal serializer As JsonSerializer)
        Dim contract = CType(serializer.ContractResolver.ResolveContract(value.GetType), JsonObjectContract)
        Dim extensionJsonProperty = Me.GetExtensionJsonProperty(contract)
        Dim jObj As JObject
        Using New PushValue(Of Boolean)(True, Function() Disabled, Function(canWrite) CSharpImpl.__Assign(Disabled, canWrite))
            jObj = JObject.FromObject(value, serializer)
        End Using
        Dim extensionValue = CType(jObj(extensionJsonProperty.PropertyName), JObject).RemoveFromLowestPossibleParent
        If (Not (extensionValue) Is Nothing) Then
            Dim i As Integer = (extensionValue.Count - 1)
            Do While (i >= 0)
                Dim prop = CType(extensionValue.AsList(i), JProperty)
                jObj.Add(prop.RemoveFromLowestPossibleParent)
                i = (i - 1)
            Loop

        End If

        jObj.WriteTo(writer)
    End Sub

    Private Class CSharpImpl
        <Obsolete("Please refactor calling code to use normal Visual Basic assignment")>
        Shared Function __Assign(Of T)(ByRef target As T, value As T) As T
            target = value
            Return value
        End Function
    End Class

    <ThreadStatic()>
    Private Shared _disabled As Boolean

    ' Disables the converter in a thread-safe manner.
    Private Property Disabled As Boolean
        Get
            Return _disabled
        End Get
        Set
            _disabled = Value
        End Set
    End Property

    Public Overrides ReadOnly Property CanWrite As Boolean
        Get
            Return Not Me.disabled
        End Get
    End Property

    Public Overrides ReadOnly Property CanRead As Boolean
        Get
            Return Not Me.disabled
        End Get
    End Property
End Class
Public Structure PushValue(Of T)
    Implements IDisposable

    Private setValue As Action(Of T)

    Private oldValue As T

    Public Sub New(ByVal value As T, ByVal getValue As Func(Of T), ByVal setValue As Action(Of T))
        If getValue Is Nothing OrElse setValue Is Nothing Then Throw New ArgumentNullException()
        Me.setValue = setValue
        Me.oldValue = getValue()
        setValue(value)
    End Sub
#Region "IDisposable Members"

    Public Sub Dispose() Implements IDisposable.Dispose
        If (Not (Me.setValue) Is Nothing) Then
            setValue(Me.oldValue)
        End If

    End Sub

#End Region
End Structure
Public Module JsonExtensions
    <Extension()>
    Public Function RemoveFromLowestPossibleParent(Of TJToken As JToken)(ByVal node As TJToken) As TJToken
        If node Is Nothing Then Return Nothing
        Dim contained = node.AncestorsAndSelf().Where(Function(t) TypeOf t.Parent Is JContainer AndAlso t.Parent.Type <> JTokenType.[Property]).FirstOrDefault()
        If contained IsNot Nothing Then contained.Remove()
        ' Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
        If TypeOf node.Parent Is JProperty Then CType(node.Parent, JProperty).Value = Nothing
        Return node
    End Function

    <Extension()>
    Public Function AsList(ByVal container As IList(Of JToken)) As IList(Of JToken)
        Return container
    End Function
End Module

For the life of me I can't figure out how to refactor the 'obsolete' CSharpImpl__Assign(Of T) function so I've left it there. I'm not sure if that has an impact.

Here is the class now updated to use the converter:

Imports Newtonsoft.Json

<JsonConverter(GetType(TypedExtensionDataConverter(Of Rootobject)))>
Public Class Rootobject
    Public Sub New()
        Item = New Dictionary(Of String, ItemObj)
    End Sub

    <JsonTypedExtensionData>
    Public Property Item As Dictionary(Of String, ItemObj)
    <JsonProperty("Errors")>
    Public Property Errors As List(Of List(Of String))
    <JsonProperty("timestamp")>
    Public Property timestamp As String
End Class

Public Class ItemObj
    Public Property config As Config
End Class

Public Class Config
    Public Property PortInfo As Portinfo
    Public Property Custom As Dictionary(Of String, Integer)
End Class

Public Class Portinfo
    Public Property baud As Integer
    Public Property dataLength As Integer
    Public Property flowControl As Integer
    Public Property parity As Integer
End Class

In my main method I have the following:

Dim Jsonstr As String = "{""Item1"":{""config"":{""PortInfo"":{""baud"":19200,""dataLength"":8,""flowControl"":0,""parity"":0},""Custom"":{""116"":1,""117"":2,""123"":1,""124"":0,""125"":2,""129"":85}}},""Item3"":{""config"":{""PortInfo"":{""baud"":19200,""dataLength"":8,""flowControl"":0,""parity"":0},""Custom"":{""116"":1,""117"":2,""123"":1,""124"":0,""125"":2,""129"":85}}},""Item4"":{""config"":{""PortInfo"":{""baud"":19200,""dataLength"":8,""flowControl"":0,""parity"":0},""Custom"":{""116"":1,""117"":2,""123"":1,""124"":0,""125"":2,""129"":85}}},""Errors"":[[""com2"",""port busy""],[""com4"",""port busy""]],""timestamp"":""2023-11-13 15:31:29""}"
Dim result = Newtonsoft.Json.JsonConvert.DeserializeObject(Of Rootobject)(Jsonstr)

At runtime, I get the error:

System.InvalidCastException: 'Unable to cast object of type 'System.Func`1[System.Object]' to type 'PrinterAudit.Rootobject'.'

Upvotes: 0

Views: 262

Answers (1)

Power Mouse
Power Mouse

Reputation: 1441

this is a C# example you can do in 2 cycles, first deserialize what is common, and second - set array of objects (i used to filter by "Item" - you can find something like not error and not timestamp or something....).

in addition, instead of list you can have a Dictionary and add name as key for reference...


void Main()
{
    string json = "{\"Item1\":{\"config\":{\"PortInfo\":{\"baud\":19200,\"dataLength\":8,\"flowControl\":0,\"parity\":0},\"Custom\":{\"116\":1,\"117\":2,\"129\":85,\"123\":1,\"124\":0,\"125\":2}}},\"Item3\":{\"config\":{\"PortInfo\":{\"baud\":19200,\"dataLength\":8,\"flowControl\":0,\"parity\":0},\"Custom\":{\"116\":1,\"117\":2,\"129\":85,\"123\":1,\"124\":0,\"125\":2}}},\"Item4\":{\"config\":{\"PortInfo\":{\"baud\":19200,\"dataLength\":8,\"flowControl\":0,\"parity\":0},\"Custom\":{\"116\":1,\"117\":2,\"129\":85,\"123\":1,\"124\":0,\"125\":2}}},\"Errors\":[[\"com2\",\"port busy\"],[\"com4\",\"port busy\"]],\"timestamp\":\"2023-11-13 15:31:29\"}";

    Rootobject result = JsonConvert.DeserializeObject<Rootobject>(json);
    
    var parsed = JObject.Parse(json);
    foreach (var el in parsed)
    {
        if (el.Key.Contains("Item"))
            result.Items.Add(el.Value.ToObject<ItemObj>());
    }   
        
    result.Dump();  
    
}

public class Rootobject
{
    public Rootobject() {
        Items = new List<ItemObj>();
    }
    public List<ItemObj> Items {get;set;}
        
    [JsonProperty("Errors")]
    public List<List<string>> Errors { get; set; }
    [JsonProperty("timestamp")]
    public string timestamp { get; set; }
}

public class ItemObj
{
    public Config config { get; set; }
}

public class Config
{
    public Portinfo PortInfo { get; set; }
    public Dictionary<string, int> Custom { get; set; }
}

public class Portinfo
{
    public int baud { get; set; }
    public int dataLength { get; set; }
    public int flowControl { get; set; }
    public int parity { get; set; }
}

result would be: enter image description here

UPDATE: you can do reverse logic: have in class

public Rootobject() {
        Items = new Dictionary<string,ItemObj>();
    }
    public Dictionary<string, ItemObj> Items {get;set;}
        

and in code:


foreach (var el in parsed)
    {
        if (!new string[] {"timestamp", "errors"}.Contains(el.Key.ToLower()))
            result.Items.Add(el.Key, el.Value.ToObject<ItemObj>());
    }


so result would be more suitable

enter image description here

Upvotes: 1

Related Questions