使用具有不同名称的相同类型的多个元素反序列化 JSON

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

提问人:NapkinBob 提问时间:11/14/2023 最后编辑:NapkinBob 更新时间:11/18/2023 访问量:48

问:

我正在尝试在 VB.net 4.5 中将一些 json 反序列化为自定义对象

Json 结构可以有多个具有不同名称的元素,并且名称可能不相同,因此我无法为每个可能的名称创建静态类,因为我不知道它们会是什么。所有子元素的结构都是相同的。我无法控制 json 的结构,它来自另一个应用程序。

下面是 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"
}

我创建了以下类来保存数据:

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

我不知道如何将此 JSON 反序列化到我的对象中,从而导致

  • 元素的字典(或某种列表)Item
  • 单个阵列Errors
  • 单个元素timestamp

我得到的最接近的是,如果我从 JSON 中删除错误和时间戳(所以它只是 s,然后我可以用(基于这里的答案)填充对象。如果我留下 and 元素,那么我会得到一个异常,说:itemresult = Newtonsoft.Json.JsonConvert.DeserializeObject(Of Dictionary(Of String, ItemObj))(Jsonstring)Errorstimestamp

无法将当前 JSON 数组(例如 [1,2,3])反序列化为类型“PrinterAudit.Rootobject”,因为该类型需要 JSON 对象(例如 {“name”:“value”})才能正确反序列化。

如果所有项目都是它们自己的数组或元素,那么这很容易,但事实上,元素与 s 处于同一级别,这真的让我失望了。ErrorstimestampItem

编辑

使用 dbc 的解决方案,我尝试转换为 VB,从而产生以下转换器:

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

在我的一生中,我无法弄清楚如何重构“过时”的函数,所以我把它留在那里。我不确定这是否有影响。CSharpImpl__Assign(Of T)

下面是现在更新为使用转换器的类:

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

在我的主要方法中,我有以下内容:

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)

在运行时,我收到错误:

System.InvalidCastException:“无法强制转换类型的对象 “System.Func”1[System.Object]“键入”PrinterAudit.Rootobject”。

json vb.net json.net 反序列化 json-deserialization

评论

1赞 dbc 11/14/2023
JSON 混合了已知属性(和 )和运行时属性,其值具有固定架构。这看起来像是如何使用动态(数字)键名反序列化子对象的副本?,同意吗?"Errors""timestamp"
0赞 NapkinBob 11/15/2023
@dbc 谢谢你。我没有遇到过。我从该答案中获取了您的通用转换器,并尝试将其转换为 VB 作为我的 C#,因为我不习惯 C#。转换时花了几刀,但我可以让它编译。它顺利完成所有操作,但在最后抛出一个异常(我开始返回值,然后反序列化调用抛出异常:我不知道我是否在转换为 VB 时做错了什么,或者其他什么。有什么建议吗?ReadJson'Unable to cast object of type 'System.Func``1[System.Object]' to type 'PrinterAudit.Rootobject'.'
1赞 dbc 11/15/2023
需要看到一个最小的可重现示例来提供帮助。只是一个猜测,但是,在 vb.net 中,如果你有一个返回函数的属性,我相信你需要两组括号才能实际调用该函数。即.原因是第一个集合是属性的可选调用。转换器往往会弄错这一点。SomeFunction()()
0赞 NapkinBob 11/15/2023
@dbc,我已经更新了问题,尝试转换您的代码,以及我添加到类中的属性。我不确定你说双括号时指的是哪个功能。ReadJson 函数是一个覆盖函数,因此不会在任何地方调用,但我可能误解了。

答:

1赞 Power Mouse 11/18/2023 #1

这是一个 C# 示例,您可以在 2 个周期内完成,首先反序列化常见内容,其次 - 设置对象数组(我曾经按“Item”过滤 - 您可以找到诸如不是错误而不是时间戳之类的东西......

此外,您可以拥有字典而不是列表,并添加名称作为参考键......


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; }
}

结果将是:enter image description here

更新:你可以做反向逻辑: 在课堂上

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

在代码中:


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


所以结果会更合适

enter image description here