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#).
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…