2

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?

Merijn
  • 661
  • 1
  • 7
  • 21
  • 1
    Is it because you're only setting PreserveReferencesHandling on serialisation, not on deserialisation, so it doesn't know how to read the $refs? – gandaliter Apr 14 '20 at 10:19
  • 1
    Please share the _exact_ JSON generated. Also, have you read https://www.newtonsoft.com/json/help/html/PreserveObjectReferences.htm ? – mjwills Apr 14 '20 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 – Merijn Apr 14 '20 at 12:38

3 Answers3

1

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

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • 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? – Merijn Apr 15 '20 at 14:51
  • 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. – Brian Rogers Apr 15 '20 at 15:12
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);
gandaliter
  • 9,863
  • 1
  • 16
  • 23
  • I updated the code fragment in the post to also use the PreserveReferencesHandling setting when deserializing. However, does does not have any effect/. – Merijn Apr 14 '20 at 12:37
  • Hmm. Pretty weird - as you say the json looks fine – gandaliter Apr 14 '20 at 12:44
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 };
gandaliter
  • 9,863
  • 1
  • 16
  • 23
  • Yes, I figured that out too. However, I do prefer my readonly model where properties only get their value at construction time. – Merijn Apr 14 '20 at 13:07
  • 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. – gandaliter Apr 14 '20 at 13:09
  • 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. https://github.com/JamesNK/Newtonsoft.Json/issues – gandaliter Apr 14 '20 at 13:10
  • 1
    Submitted https://github.com/JamesNK/Newtonsoft.Json/issues/2315 – Merijn Apr 14 '20 at 13:18
  • I'm not sure if I fully understand your proposal to make the model pure. Can you explain a bit more? – Merijn Apr 14 '20 at 13:20
  • At the moment you're modifying the list of children (of a `Parent`) after construction, because both `Parent` and `Child` reference the other. If I were writing this I would want that `IList` to be a `IReadOnlyList`, and `Child` not to have a `Parent` property at all. – gandaliter Apr 14 '20 at 13:22
  • But then you wouldn't have a way to find a `Parent` from its `Child`, so I would add a getter on `Parent` that takes the list and does `children.Select(c => new ChildWrapper(c, Id).ToImmutableList()` or something like that. – gandaliter Apr 14 '20 at 13:23
  • And then rather than passing around `Child`, which doesn't know about `Parent`, you'd pass around `ChildWrapper`, which knows about both. – gandaliter Apr 14 '20 at 13:23
  • And all your models would be fully immutable, and you could serialise `Parent` without `PreserveReferencesHandling`, and without losing any functionality. – gandaliter Apr 14 '20 at 13:24
  • Interesting idea! The classes in my model get instantiated by entity framework or by the json deserializer. I think your approach requires some boilerplate code to get it to work with both frameworks, right? – Merijn Apr 14 '20 at 15:11
  • I don't know EF, but Newtonsoft certainly does work with getter-only properties initialised from the constructor, as I do that nearly everywhere. It's not that conventional for C# though, and I've encountered libraries that didn't work with it. – gandaliter Apr 14 '20 at 15:13