Reputation: 724
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
Item
elementsErrors
arraytimestamp
elementThe closest I got was if I remove the errors and timestamp from the JSON (so it's only the item
s, 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 Item
s, 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
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; }
}
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
Upvotes: 1