3

I've a simple relational model with a Parent and Child relation as follows:

    public class Parent
    {
        public Parent(int id)
        {
            Id = id;
        }

        public int Id { get; private set; }
        public IList<Child> Children { get; set; } = new List<Child>();
    }

    public class Child
    {
        public Parent Parent { get; set; }
    }

I create a small object graph consisting of a list of two children who share the same parent:

    var parent = new Parent(1);
    var child1 = new Child {Parent = parent};
    var child2 = new Child {Parent = parent};
    parent.Children.Add(child1);
    parent.Children.Add(child2);

    var data = new List<Child> {child1, child2};

Next, I serialize this using SerializeObject:

    var settings = new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.All
    };
    var json = Newtonsoft.Json.JsonConvert.SerializeObject(data, Formatting.Indented, settings);

As far I can see, the resulting json looks fine:

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "Parent": {
        "$id": "3",
        "Id": 1,
        "Children": {
          "$id": "4",
          "$values": [
            {
              "$ref": "2"
            },
            {
              "$id": "5",
              "Parent": {
                "$ref": "3"
              }
            }
          ]
        }
      }
    },
    {
      "$ref": "5"
    }
  ]
}

However, when I deserialize the json I don't get the expected object because for the second child the Parent property is null, causing the second assertion to fail:

    var data2 = Newtonsoft.Json.JsonConvert.DeserializeObject<IList<Child>>(json, settings);
    Debug.Assert(data2[0].Parent != null);
    Debug.Assert(data2[1].Parent != null);

Without the constructor of Parent this problem does not occur and the Parent property of the second child has the expected value.

Any ideas what this could be?

3
  • 1
    Is it because you're only setting PreserveReferencesHandling on serialisation, not on deserialisation, so it doesn't know how to read the $refs? Commented Apr 14, 2020 at 10:19
  • 1
    Please share the exact JSON generated. Also, have you read newtonsoft.com/json/help/html/PreserveObjectReferences.htm ? Commented Apr 14, 2020 at 10:20
  • I updated the code to also use PreserveReferencesHandling when deserializing. However, this doesn't have any efect I also included the resulting JSON Commented Apr 14, 2020 at 12:38

3 Answers 3

2

Parameterized constructors don't work well with PreserveReferencesHandling because there is an inherent chicken-and-egg problem: the necessary information to pass to the constructor may not be loaded from the JSON by the time the deserializer needs to create the object. So there needs to be a way for it to create an empty object and then fill in the appropriate information later.

This is a known limitation and is documented:

Note:
References cannot be preserved when a value is set via a non-default constructor. With a non-default constructor, child values must be created before the parent value so they can be passed into the constructor, making tracking reference impossible. ISerializable types are an example of a class whose values are populated with a non-default constructor and won't work with PreserveReferencesHandling.

To work around the problem with your model you could create a private, parameterless constructor in your Parent class and mark it with [JsonConstructor]. Then mark the Id property with [JsonProperty], which will allow Json.Net to use the private setter. So you would have:

public class Parent
{
    public Parent(int id)
    {
        Id = id;
    }

    [JsonConstructor]
    private Parent()
    { }

    [JsonProperty]
    public int Id { get; private set; }
    public IList<Child> Children { get; set; } = new List<Child>();
}

As an aside, since none of your Lists are shared amongst the objects, you could use PreserveReferencesHandling.Objects instead of PreserveReferencesHandling.All. This will make the JSON a little smaller, but will still preserve the references you care about.

Working demo here: https://dotnetfiddle.net/cLk9DM

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for this detailed explanation. I was considering these attributes myself, however I don't feel very happy about "polluting" my data model with hints to a particular serializer. What is your opinion about this?
Ha, well, I'm a bit more pragmatic about these things I guess. If adding a couple of attributes is all that is standing in the way of getting my job done and it doesn't otherwise interfere with my design in any way, I'll take the attributes. However, that is just me. You ultimately have to decide what is best for you and your code, since you will be the one maintaining it. I can understand why you might not want to take a dependency to Json.Net in your model library. In that case, I would suggest you do not serialize your models at all-- instead map them to DTOs and serialize those.
0

I haven't tested this, but I have a feeling it'll solve it if you put the settings into both serialiser and deserialiser:

var settings = new JsonSerializerSettings
{
    PreserveReferencesHandling = PreserveReferencesHandling.All
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(data, Formatting.Indented, settings);
var data2 = Newtonsoft.Json.JsonConvert.DeserializeObject<IList<Child>>(json, settings);

2 Comments

I updated the code fragment in the post to also use the PreserveReferencesHandling setting when deserializing. However, does does not have any effect/.
Hmm. Pretty weird - as you say the json looks fine
0

Second attempt:

I reproduced the problem, and then changed the constructor of Parent to be parameterless, and the Id property to be settable, and then it worked. No idea why, but I guess it's some bug in the way Newtonsoft chooses how to construct things, especially as the order will be important with PreserveReferencesHandling.

public class Parent
{
    public int Id { get; set; }
    public IList<Child> Children { get; set; } = new List<Child>();
}

and

var parent = new Parent { Id = 1 };

11 Comments

Yes, I figured that out too. However, I do prefer my readonly model where properties only get their value at construction time.
If you like your models to be pure then have you considered not having them reference each other? You could have a getter on Parent that takes the children and returns a different class wrapping Child with a reference to its Parent, and that would cover all use cases as well as being serialisable in the normal way.
In terms of the actual problem I think it's probably a bug in Newtonsoft, so might be worth opening an issue, if there isn't one already. github.com/JamesNK/Newtonsoft.Json/issues
I'm not sure if I fully understand your proposal to make the model pure. Can you explain a bit more?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.