假设我有两种类型,一种Document
,一种Child
。Child
嵌套在Document
中相当深,并且包含对父级的反向引用,该引用必须传递给其构造函数。 如何使用 Json.NET 反序列化此类对象图并将父项传递到子构造函数中?
下面是一个具体的例子,灵感来自Ama 反序列化为 List(Of T) 时的 Pass 构造函数参数:
Class Document
Public Property MyObjects as List(Of Child) = new List(Of Child)()
End Class
Class Child
Private ReadOnly _Parent As Document
Sub New(Parent As Document)
_Parent = Parent
End Sub
Property Foo As String
Property Bar As String
Function GetParent() As Document
Return _Parent
End Function
End Class
使用相应的 JSON:
{
"MyObjects": [
{
"Foo": "foo",
"Bar": "bar"
}
]
}
笔记:
Child
中的父引用是只读的,必须传递到构造函数中。我无法修改
Document
和Child
的类定义。Document
和Child
比此处显示的更复杂,因此不建议加载到JToken
层次结构中然后手动构造。
如何将 JSON 反序列化为此类数据模型,并在正确初始化父级的情况下构建子项列表?
由于无法修改Document
和Child
的定义,因此执行此操作的一种方法是使用自定义协定解析程序,该解析程序返回跟踪某些ThreadLocal(Of Stack(Of Document))
堆栈中正在反序列化的当前文档的协定,并使用最顶层的文档分配MyObject
实例。
以下协定解析器完成这项工作:
Public Class DocumentContractResolver
Inherits DefaultContractResolver
Private ActiveDocuments As ThreadLocal(Of Stack(Of Document)) = New ThreadLocal(Of Stack(Of Document))(Function() New Stack(Of Document))
Protected Overrides Function CreateContract(ByVal objectType As Type) As JsonContract
Dim contract = MyBase.CreateContract(objectType)
Me.CustomizeDocumentContract(contract)
Me.CustomizeMyObjectContract(contract)
Return contract
End Function
Private Sub CustomizeDocumentContract(ByVal contract As JsonContract)
If GetType(Document).IsAssignableFrom(contract.UnderlyingType) Then
contract.OnDeserializingCallbacks.Add(Sub(o, c) ActiveDocuments.Value.Push(CType(o, Document)))
contract.OnDeserializedCallbacks.Add(Sub(o, c) ActiveDocuments.Value.Pop())
End If
End Sub
Private Sub CustomizeMyObjectContract(ByVal contract As JsonContract)
If (GetType(Child) = contract.UnderlyingType) Then
contract.DefaultCreator = Function() New Child(ActiveDocuments.Value.Peek())
contract.DefaultCreatorNonPublic = false
End If
End Sub
End Class
然后像这样使用它:
Dim contractResolver = New DocumentContractResolver() ' Cache this statically somewhere
Dim settings = New JsonSerializerSettings() With { .ContractResolver = contractResolver }
Dim doc2 = JsonConvert.DeserializeObject(Of Document)(jsonString, settings)
在 c# 中:
public class DocumentContractResolver : DefaultContractResolver
{
ThreadLocal<Stack<Document>> ActiveDocuments = new ThreadLocal<Stack<Document>>(() => new Stack<Document>());
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
CustomizeDocumentContract(contract);
CustomizeMyObjectContract(contract);
return contract;
}
void CustomizeDocumentContract(JsonContract contract)
{
if (typeof(Document).IsAssignableFrom(contract.UnderlyingType))
{
contract.OnDeserializingCallbacks.Add((o, c) => ActiveDocuments.Value.Push((Document)o));
contract.OnDeserializedCallbacks.Add((o, c) => ActiveDocuments.Value.Pop());
}
}
void CustomizeMyObjectContract(JsonContract contract)
{
if (typeof(Child) == contract.UnderlyingType)
{
contract.DefaultCreator = () => new Child(ActiveDocuments.Value.Peek());
contract.DefaultCreatorNonPublic = false;
}
}
}
笔记:
如果在反序列化期间发生异常,则可能无法正确清除
ActiveDocuments
。 您可能需要添加序列化错误处理程序来执行此操作。正如Newtonsoft的性能提示中所解释的那样,
若要避免每次使用 JsonSerializer 时重新创建协定的开销,应创建一次协定解析程序并重复使用它。
ThreadLocal<T>
是一次性的,所以如果你不打算缓存你的WordContractResolver
你可能也应该让它一次性,并在dispose方法中释放threadlocal。
演示小提琴在这里 (vb.net) 和这里 (c#)。