【问题标题】:Pass additional data to JsonConverter将附加数据传递给 JsonConverter
【发布时间】:2019-04-11 02:54:33
【问题描述】:

我反序列化其中一个属性是外键的对象(例如,来自数据库表的标识值)。在反序列化期间,我想使用 JsonConverter 从集合中检索相应的对象。

我知道如何编写一个使用自定义的 JsonConverters。我不知道如何将集合传递给 JsonConverter,因为转换器是在设计时指定的(如下所示),但集合显然只存在于运行时:

  <JsonConverter(GetType(JSonCustomConverter))>
  Public Property SomeProperty As SomePropertyClass

所以 JSonCustomConverter 的 ReadJson 应该是这样的:

Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return False
  Dim value As String = reader.Value.ToString().Trim()
  retun MagicallyGetMyCollectionValue(value)
End Function

因此,愚蠢的函数名称 MagicallyGetMyCollectionValue 只是一个占位符,向您展示我被困在哪里。我不想通过全局变量访问集合,但我也不知道如何将集合传递给 ReadJson。

如果有人能指出我正确的方向,我会很高兴。


编辑:让我试着举一个更好的例子。

假设我有以下课程:

class ParentObject
  <JssonConverter(GetType(JsonCustomConverter))>
  Property SomeProperty As SomePropertyClass
end class

我会像这样反序列化我的 json 数据:

 dim result = JsonConvert.DeserializeObject(jsonData, GetType(ParentObject))

现在假设,json 数据不包含 SomePropertyClass 实例的完整表示,而仅包含键值,例如作为字符串的键。假设我有一个这样的集合:

dim cache as Dictionary(of string, SomePropertyClass)

该缓存应包含我需要的所有实例。所以我的 JSONCustomConverter 应该有这样的 ReadJson 函数:

Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return nothing
  Dim value As String = reader.Value.ToString().Trim()
  Dim cachedObject as SomePropertyClass = nothing
  if cache.TryGetValue(value, cachedObject) then return cachedObject
  retun Nothing ' or new SomePropertyClass(value)
End Function

所以我希望 ReadJson 根据键值查找实例。

如何将缓存字典传递给 ReadJson 函数?我可以使用包含缓存的单例类和 som getInstance 方法来检索它,但我不想这样做。

【问题讨论】:

  • @Amessihel:不,我不这么认为,因为该解决方案显示了如何基于 json 数组反序列化为一个或另一个类。就我而言,我知道将我的属性反序列化到的类。我尝试在问题中添加一个更好的示例,以便更清楚地说明我想要什么(这需要几分钟)。
  • 假设您正在自己构建序列化程序,您可以使用StreamingContext.Context 通过JsonSerializer.Context 传递附加数据。 In JSON.NET how to get a reference to every deserialized object? 显示了一个示例。
  • @dbc 这看起来很有希望。我会试一试,并就它的工作原理发表评论。
  • 如果它确实有效,你能帮我一个忙并发布解决方案作为答案吗?
  • @doom87er -- 正在开发一个。

标签: vb.net json.net


【解决方案1】:

您可以通过JsonSerializer.Context 使用StreamingContext.Context 将其他数据传递给您的自定义JsonConverter。使用这种机制,可以以通用方式将类实例映射到名称。

首先,定义以下接口和泛型转换器:

Public Interface ISerializationContext
    Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface

Public Interface INameTable(Of T)
    Function TryGetName(value As T, ByRef name as String) As Boolean    
    Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface

Public Class NameTable(Of T) :  Implements INameTable(Of T)
    Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()

    Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()

    Public Function Add(value as T, name as String) as T
        Dictionary.Add(name, value)
        ReverseDictionary.Add(value, name)              
        Return value
    End Function

    Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
        Return ReverseDictionary.TryGetValue(value, name)
    End Function

    Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
        Return Dictionary.TryGetValue(name, value)
    End Function
End Class

Public Class ObjectToNameConverter(Of T)
    Inherits JsonConverter

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return GetType(T) = objectType
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Dim tValue = CType(value, T)
        Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
        If context Is Nothing
            Throw New JsonSerializationException("No ISerializationContext.")
        End If

        Dim nameTable as INameTable(Of T) = Nothing
        If (Not context.TryGetNameTable(Of T)(nameTable))
            Throw New JsonSerializationException("No NameTable.")
        End If

        Dim name as String = Nothing
        if (Not nameTable.TryGetName(tValue, name))
            Throw New JsonSerializationException("No Name.")
        End If

        writer.WriteValue(name)
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
        If context Is Nothing
            Throw New JsonSerializationException("No ISerializationContext.")
        End If

        Dim nameTable as INameTable(Of T) = Nothing
        If (Not context.TryGetNameTable(Of T)(nameTable))
            Throw New JsonSerializationException("No NameTable.")
        End If

        Dim name As String = serializer.Deserialize(Of String)(reader)
        If name Is Nothing Then
            Return Nothing
        End If

        dim tValue as T = Nothing
        nameTable.TryGetValue(name, tValue)
        return tValue
    End Function
End Class

接下来,定义以下具体实现:

Public Class RootObject
    <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
    Public Property SomeProperty As SomePropertyClass   
End Class

Public Class SomePropertyClass
End Class

Public Class MySerializationContext : Implements ISerializationContext
    Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
        Return SomePropertyNameTable.Add(value, name)
    End Function

    Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)

    Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
        if (GetType(T) Is GetType(SomePropertyClass))
            table = SomePropertyNameTable
            return True
        End If

        table = Nothing
        return False
    End Function
End Class   

现在,您可以在反序列化期间将 SomePropertyClass 的实例替换为其名称,如下所示:

Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
    .Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty) 
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))

注意事项:

  • ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) 是通用的,因此可以同时支持多种类型的对象的对象到名称替换,而不会相互干扰。

    然而,具体的实现不必如此通用。这里MySerializationContext 仅支持SomePropertyClass 实例的名称替换。其他可以根据需要添加。

  • Does Json.NET cache types' serialization information? 中所述,Newtonsoft 建议缓存 DefaultContractResolver 及其子类型的实例以获得最佳性能。因此,通过StreamingContext.Context 而不是通过新分配的DefaultContractResolver 子类实例来传递附加数据可能更可取。

.Net fiddle #1 here 的示例工作。

作为替代,虽然上述设计有效,但我认为从SomeProperty 中删除&lt;JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))&gt; 并传递一个适当初始化的ObjectToNameConverter(Of SomePropertyClass) 会更简单,其中包含本地引用给一些INameTable(Of SomePropertyClass),在JsonSerializerSettings.Converters

像这样定义转换器和接口。请注意,ObjectToNameConverter(Of T) 现在有一个参数化构造函数,并且不再需要 ISerializationContext

Public Interface INameTable(Of T)
    Function TryGetName(value As T, ByRef name as String) As Boolean    
    Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface

Public Class NameTable(Of T) :  Implements INameTable(Of T)
    Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()

    Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()

    Public Function Add(value as T, name as String) as T
        Dictionary.Add(name, value)
        ReverseDictionary.Add(value, name)              
        Return value
    End Function

    Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
        Return ReverseDictionary.TryGetValue(value, name)
    End Function

    Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
        Return Dictionary.TryGetValue(name, value)
    End Function
End Class

Public Class ObjectToNameConverter(Of T)
    Inherits JsonConverter

    Private Property NameTable as INameTable(Of T)

    Public Sub New(nameTable as INameTable(Of T))
        If nameTable Is Nothing 
            Throw new ArgumentNullException("nameTable")
        End If
        Me.NameTable = nameTable
    End Sub

    Public Overrides Function CanConvert(objectType As Type) As Boolean
        Return GetType(T) = objectType
    End Function

    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Dim tValue = CType(value, T)

        Dim name as String = Nothing
        if (Not NameTable.TryGetName(tValue, name))
            Throw New JsonSerializationException("No Name.")
        End If

        writer.WriteValue(name)
    End Sub

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim name As String = serializer.Deserialize(Of String)(reader)
        If name Is Nothing Then
            Return Nothing
        End If

        dim tValue as T = Nothing
        NameTable.TryGetValue(name, tValue)
        return tValue
    End Function
End Class

然后序列化如下:

dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }

Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))

Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty) 
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))

以这种方式做事消除了静态序列化方法对第一个解决方案中存在的运行时代码的依赖。现在所有名称替换逻辑都在运行时在一个位置处理。

小提琴样本 #2 here.

【讨论】:

    【解决方案2】:

    根据@doom87er 的要求,我将分享对我有用的代码。该解决方案基于@dbc 的评论,并进行了一些更改。请把下面的代码更像是概念代码:我不得不更改一些名称并省略一些逻辑,这不是这个概念证明所必需的。所以可能有错别字。

    主要的解决方案是继承 DefaultContractResolver 并将缓存字典添加到该类。像这样的:

    Public Class CacheContractResolver
        Inherits DefaultContractResolver
        Public Cache As Dictionary(of string, SomePropertyClass)
    
        Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
            Me.Cache = preFilledCache 
        End Sub
    End Class
    

    然后您使用 JsonSerializerSettings 传递自定义合同解析器,如下所示:

    Dim settings = New JsonSerializerSettings
    settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
    Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
    

    其中 prefilledCache 是包含 SomePropertyClass-objects 的字典实例。

    最后一步是在我的 JsonConverter 的 ReadJson 函数中检索缓存(我附加到 SomeProperty,如原始帖子的示例代码所示):

    Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
      Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
      if cacheResolver is nothing return nothing ' add some better null handling here
    
      Dim value As String = reader.Value.ToString().Trim()
      Dim cachedObject as SomePropertyClass = nothing
      if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
      retun Nothing ' or new SomePropertyClass(value)
    End Function
    

    我试过了,它似乎有效。

    简而言之:

    1. 继承 DefaultContractResolver 并包含您需要的所有其他数据。
    2. 通过 JsonSerializerSettings 中的附加数据传递自定义合同解析器的实例。
    3. 在您的 JsonConverter trycast 中,将传递的合同解析器返回到您的自定义合同解析器,然后您就有了额外的数据。

    如果您对我可能错过的任何捕获发表评论,我会很高兴,但我认为这应该是我可以接受的解决方案。

    感谢您的评论和帮助。 萨沙

    【讨论】:

    • 谢谢。我遇到了一些非常复杂的转换器的堆栈溢出问题。希望我能解决这个问题
    猜你喜欢
    • 1970-01-01
    • 2013-03-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-21
    • 2013-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多