Deserializing Circular References by Two-Phase deserialization

痞子三分冷 提交于 2019-12-13 03:50:03

问题


I have a Serializer/Deserializer that use a references PreserveReferencesHandling = PreserveReferencesHandling.All.

The issue, is that I have circular references.

Here is a very simple example.

class Node
{
    public Node(object value)
    {
        Value = value;
    }
    public object Value { get; set; }
    public Node Left { get; set; }
    public Node Right { get; set; }
}

My test scenario is:

var obj = new Node("o")
{
    Left = new Node("oL"),
    Right = new Node("oR")
};
obj.Right.Right = obj; // Circular reference!

When I deserialize, i have the following IReferenceResolver

private class InternalReferenceResolver : IReferenceResolver
{
    private readonly Deserializer _par;

    public InternalReferenceResolver(Deserializer par)
    {
        _par = par;
    }

    public object ResolveReference(object context, string reference)
    {
        object refObject;
        if (!_par._processed.TryGetValue(reference, out refObject))
        {
            refObject = _par.DeserializeObject(reference);
        }
        return refObject;
    }

    public string GetReference(object context, object value)
    {
        throw new NotSupportedException("Only for Serialization");
    }

    public bool IsReferenced(object context, object value)
    {
        return false;
    }

    public void AddReference(object context, string reference, object value)
    {
        _par._processed.Add(reference, value);
    }
}    

As you can see, when JSON.NET inform me of a new ref->object (via AddReference()) I add it to a dictionary.

When JSON.NET request an object for a specific reference (via ResolveReference()) I recurse, and deserialize that reference.

The issue is, JSON.NET calls ResolveReference() for each of the object references, before it calls it's AddReference().

I would expect the flow of Deserialization to be:

  1. Identify Object Type
  2. Construct the object
  3. AddReference(id, newObj)
  4. Resolve References + Populate Properties

What I see happens is:

  1. Identify Object Type
  2. Resolve References
  3. Construct the object
  4. AddReference(id, newObj)
  5. Populate Properties

My Questions:

  1. Why is it made the latter, am i missing something with my suggested flow?

  2. how can I overcome this issue, having a "Bare" object just for referencing and only then actually resolve the references ?


回答1:


The Two-Phase deserialization you are seeing is arises because your Node class only has a parameterized constructor. As explained in Issue with serializing/deserializing object graph with self-referencing objects in combination with JSON constructor. #715:

JamesNK commented on Nov 28, 2015

Non-default constructors and preserving references don't work well together because the child values of a type have to be deserialized before the parent is created, so the reference resolves to null.

Thus, since Value is a mutable property anyway, you should add a parameterless constructor to Node:

class Node
{
    public Node() : this(null) { }

    public Node(object value)
    {
        Value = value;
    }
    // Remainder unchanged.
}

It can be non-public if you mark it with [JsonConstructor] or deserialize using the setting ConstructorHandling.AllowNonPublicDefaultConstructor. And if Value were immutable, you would need to make it privately settable and mark it with [JsonProperty]

[JsonProperty]
public object Value { get; private set; }

(Data contract attributes can be used in place of Json.NET attributes if you prefer.)

Notes:

  • Since your question lacks a complete and verifiable example, your code might encounter other problems that aren't fixed by adding a parameterless constructor.

  • For a related question, see Usage of non-default constructor breaks order of deserialization in Json.net.

  • For a related issue, see PreserveReferencesHandling.Objects deserialize does not work with non-default constructor #678.

  • Another thing, can I tell Json.NET to only handle the constructor parameters and leave the rest... ?

    Not according to Issue 715. Since a JSON object is an unordered set of name/value pairs, Json.NET needs to parse the entire object to ensure it has loaded all the constructor parameters. As it is a single-pass serializer the non-constructor parameters will get loaded into... something... before the object can be constructed. Json.NET has chosen to deserialize them in one step to the final target member type rather than to an intermediate JToken and later the final member type. This can be see in JsonSerializerInternalReader.ResolvePropertyAndCreatorValues().




回答2:


Well, I Found a solution for my problem:

The first Deserialization i perform, i use a custom IContractResolver that exclude all properties that are not relevant to the constructor...

at the second pass, I use Populate and use the default IContractResolver

    private class InternalOnlyCtorContractResolver : IContractResolver
    {
        private readonly IContractResolver _base;

        public InternalOnlyCtorContractResolver(IContractResolver _base)
        {
            this._base = _base;
        }

        public JsonContract ResolveContract(Type type)
        {
            var contract = _base.ResolveContract(type);
            var objectContract = contract as JsonObjectContract;
            if (objectContract != null)
            {
                var creatorParameters = new HashSet<string>(objectContract.CreatorParameters.Select(p => p.PropertyName));
                var irrelevantProperties = objectContract.Properties
                    .Where(p => !creatorParameters.Contains(p.PropertyName))
                    .ToArray();
                foreach (var irrelevantProperty in irrelevantProperties)
                {
                    objectContract.Properties.Remove(irrelevantProperty);
                }
                //TODO Can be optimized better
            }
            return contract;
        }
    }

If for some reason, the Circular Reference is needed by the Constructor,
It still cause a loop, but it is kinda impossible to create without having a second constructor anyway.



来源:https://stackoverflow.com/questions/52563281/deserializing-circular-references-by-two-phase-deserialization

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!