问题
Let's say I have two types, a Document
and a Child
. The Child
is nested fairly deeply within the Document
, and contains a back-reference to the parent that must needs be passed into its constructor. How can I deserialize such an object graph with Json.NET and pass the parent into the child's constructor?
Here's a concrete example, inspired by Pass constructor arguments when deserializing into a List(Of T) by Ama:
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
With the corresponding JSON:
{
"MyObjects": [
{
"Foo": "foo",
"Bar": "bar"
}
]
}
Notes:
The parent reference in
Child
is read-only and must be passed into the constructor.I cannot modify the class definitions for
Document
andChild
.Document
andChild
are more complicated that shown here, so loading into aJToken
hierarchy then constructing manually is not preferred.
How can I deserialize JSON to such a data model, constructing the list of children with the parent properly initialized?
回答1:
Since the definitions for Document
and Child
cannot be modified, one way to do this would be with a custom contract resolver that returns contracts that track the current document being deserialized in some ThreadLocal(Of Stack(Of Document)) stack, and allocate instances of MyObject
using the topmost document.
The following contract resolver does the job:
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
And then use it like:
Dim contractResolver = New DocumentContractResolver() ' Cache this statically somewhere
Dim settings = New JsonSerializerSettings() With { .ContractResolver = contractResolver }
Dim doc2 = JsonConvert.DeserializeObject(Of Document)(jsonString, settings)
And in 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;
}
}
}
Notes:
If an exception occurs during deserialization the
ActiveDocuments
might not get cleared properly. You might want to add a serialization error handler to do that.As explained in Newtonsoft's performance tips,
To avoid the overhead of recreating contracts every time you use JsonSerializer you should create the contract resolver once and reuse it.
ThreadLocal<T> is disposable, so if you don't plan to cache your
WordContractResolver
you should probably make it disposable also, and dispose of the threadlocal in the dispose method.
Demo fiddles here (vb.net) and here (c#).
来源:https://stackoverflow.com/questions/56316631/how-can-i-deserialize-instances-of-a-type-that-has-read-only-back-references-to