JamesNK / Newtonsoft.Json

Json.NET is a popular high-performance JSON framework for .NET
https://www.newtonsoft.com/json
MIT License
10.78k stars 3.26k forks source link

Deserialized cyclic references are null #1284

Open Alois-xx opened 7 years ago

Alois-xx commented 7 years ago

I was trying to test the reference handling of JSON.NET and either it is a feature or I have not yet found the right settings for it. I would expect that when I set PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize,

that cyclic references are serialized and deserialized. Serialization works without failure but the the deserialized cyclic field is nulled out. Am I missing something? I am using Nuget package 10.0.2. `

  [DataContract]
    public class DataForDataContract
    {
        [DataMember]
        public int Id { get; set; }

        [DataMember]
        public DataForDataContract Parent;

        [DataMember]
        private string Message;

        public DataForDataContract(int id, string message)
        {
            Id = id;
            Message = message;
        }
    }

    static void JsonNET()
    {
        var ser = JsonSerializer.Create(new JsonSerializerSettings()
        {
            PreserveReferencesHandling = PreserveReferencesHandling.Objects,
            ReferenceLoopHandling = ReferenceLoopHandling.Serialize,

        });
        var memory = Memory;
        var parent = new DataForDataContract(1, "Hello");
        parent.Parent = parent;
        var textWriter = new StreamWriter(memory);
        ser.Serialize(textWriter, parent);
        textWriter.Flush();
        memory.Position = 0;
        using (var textReader = new StreamReader(memory))
        {
            var copy = (DataForDataContract)ser.Deserialize(textReader, typeof(DataForDataContract));
            if( copy.Parent == null )
            {
                throw new NullReferenceException("copy.Parent is null");
            }
        }
    }`
mohannad-abwah commented 7 years ago

I've come across the same problem. Is this a known issue?

It seems to only happen at the top level, since this dotnetfiddle seems to have the cyclical references working fine after the first level. By that I mean the root object's parent field is null, but the childrens' parents fields have proper references.

P.S. I've simplified the demo code a bit.


void Main()
{
    var ser = new JsonSerializerSettings()
    {
        PreserveReferencesHandling = PreserveReferencesHandling.Objects,
        ReferenceLoopHandling = ReferenceLoopHandling.Serialize
    };
    var parent = new DataForDataContract(1, "Hello");
    parent.Parent = parent;
    string json = JsonConvert.SerializeObject(parent, ser); // works great!
    var deserializedObject = JsonConvert.DeserializeObject<DataForDataContract>(json);
    if (deserializedObject.Parent == null)
        throw new NullReferenceException("copy.Parent is null"); // execution reaches here
}

public class DataForDataContract
{
    public int Id { get; set; }
    public DataForDataContract Parent;
    private string Message;
    public DataForDataContract(int id, string message)
    {
        Id = id;
        Message = message;
    }
}
CatoLeanTruetschel commented 6 years ago

I habe managed to isolate the problem somehow. The problem only occurs if the the type with the cyclic reference (DataForDataContract in the example) has no default constructor. In my research, it did not matter whether the non-parameterless constructor contains a parameter for the cyclic reference itself. For example, the above example just works fine, if a default constructor is added to the DataForDataContract type.

static void Main()
{
    var ser = new JsonSerializerSettings()
    {
        PreserveReferencesHandling = PreserveReferencesHandling.Objects,
        ReferenceLoopHandling = ReferenceLoopHandling.Serialize
    };
    var parent = new DataForDataContract(1, "Hello");
    parent.Parent = parent;
    string json = JsonConvert.SerializeObject(parent, ser); // works great!
    var deserializedObject = JsonConvert.DeserializeObject<DataForDataContract>(json, ser);
    if (deserializedObject.Parent == null)
        throw new NullReferenceException("copy.Parent is null"); // Is not triggered now
}

public class DataForDataContract
{
    public int Id { get; set; }
    public DataForDataContract Parent;
    private string Message;

    public DataForDataContract(int id, string message)
    {
        Id = id;
        Message = message;
    }

    public DataForDataContract()  { } // Default constructor available
}

The example at dotnetfiddle does not work either, if the default constructor is removed. I created a fork here.

EDIT: Forgot to pass serializer settings to JsonConvert.DeserializeObject. Problem is not gone though.

CatoLeanTruetschel commented 6 years ago

Actually it is the object more closer to the root that has to be created with a default constructor in order that all references are set correctly. See this exemple.

static void Main()
{
    var ser = new JsonSerializerSettings()
    {
        PreserveReferencesHandling = PreserveReferencesHandling.Objects,
        ReferenceLoopHandling = ReferenceLoopHandling.Serialize
    };
    var b = new B(1, "Hello");

    var a = new A(1.234) { B = b };
    b.As = new List<A> { a };

    var aChild = new AChild(1.45f) { A = a };

    a.Child = aChild;

    string json = JsonConvert.SerializeObject(b, ser); // works great!
    var deserializedObject = JsonConvert.DeserializeObject<B>(json, ser);
    if (deserializedObject.As[0] == null)
        throw new NullReferenceException();

    if (deserializedObject.As[0].B == null)
        throw new NullReferenceException();

    if (deserializedObject.As[0].Child == null)
        throw new NullReferenceException();

    if (deserializedObject.As[0].Child.A == null)
        throw new NullReferenceException();
}

public class B
{
    public int Id { get; set; }
    private string Message;

    public List<A> As;

    public B(int id, string message)
    {
        Id = id;
        Message = message;
    }

    public B() // Must be present to deserialize A -> B
    {

    }
}

public class A
{
    public double D { get; set; }

    public B B { get; set; }

    public AChild Child { get; set; }

    public A(double d)
    {
        D = d;
    }

    public A() // Must be present to deserialize AChild -> A
    {

    }
}

public class AChild
{
    public A A { get; set; }
    public float F { get; set; }

    //public AChild() // Can be removed
    //{

    //}

    public AChild(float f)
    {
        F = f;
    }
}

EDIT: Forgot to pass serializer settings to JsonConvert.DeserializeObject. Problem is not gone though.

waldnercharles commented 2 years ago

Are there any workarounds for this issue aside from adding parameterless constructors?

0qln commented 1 month ago

For any future readers: