3

When deserializing an object graph with parent child relationships using Json.net, the use of non-default constructors breaks the order of deserialization such that child objects are deserialized (constructed and properties assigned) before their parents, leading to null references.

From experimentation it appears that all non-default-constructor objects are instantiated only after all default-constructor objects, and oddly it seems in the reverse order to the serialization (children before parents).

This causes 'child' objects that should have references to their parents (and are correctly serialized) to instead be deserialized with null values.

This seems like a very common scenario, so I wonder if I have missed something?

Is there a setting to change this behaviour? Is it somehow by design for other scenarios? Are there workarounds besides creating default constructors across the board?

A simple example with LINQPad or DotNetFiddle:

void Main()
{
    var root = new Root();
    var middle = new Middle(1);
    var child = new Child();

    root.Middle = middle;
    middle.Root = root;
    middle.Child = child;
    child.Middle = middle;

    var json = JsonConvert.SerializeObject(root, new JsonSerializerSettings
    {
        Formatting = Newtonsoft.Json.Formatting.Indented,
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        PreserveReferencesHandling = PreserveReferencesHandling.All,        
        TypeNameHandling = TypeNameHandling.All,
    });

    json.Dump();

    //I have tried many different combinations of settings, but they all
    //seem to produce the same effect: 
    var deserialized = JsonConvert.DeserializeObject<Root>(json);

    deserialized.Dump();
}

public class Root
{
    public Root(){"Root".Dump();}

    public Middle Middle {get;set;}
}

public class Middle
{
    //Uncomment to see correct functioning:
    //public Middle(){"Middle".Dump();}

    public Middle(int foo){"Middle".Dump();}

    public Root Root {get;set;}

    public Child Child {get;set;}
}

public class Child
{
    public Child(){"Child".Dump();}

    public Middle Middle {get;set;}
}

JSON output:

{
  "$id": "1",
  "$type": "Root",
  "Middle": {
    "$id": "2",
    "$type": "Middle",
    "Root": {
      "$ref": "1"
    },
    "Child": {
      "$id": "3",
      "$type": "Child",
      "Middle": {
        "$ref": "2"
      }
    }
  }
}

Output with Middle having non-default constructor:

Root
Child
Middle
Child.Middle = null

Output with Middle having default constructor:

Root
Middle
Child
Child.Middle = Middle
Andrew Hanlon
  • 7,271
  • 4
  • 33
  • 53
  • @JonSkeet Thanks, added! – Andrew Hanlon Apr 26 '16 at 13:35
  • 1
    Which version of Json.NET are you using? – dbc Apr 26 '16 at 16:21
  • @dbc It occurs in the latest nuget package (8.0.3), and I have tested it all the back on 5.0.8 with the same issue - which leads me to think it is by design (though one I do not understand). – Andrew Hanlon Apr 26 '16 at 16:28
  • 1
    The [release notes for 7.0.1](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/7.0.1) include a note *Fix - Fixed preserving object references with read only properties* -- but nevertheless it's not fixed in that version or later. – dbc Apr 26 '16 at 16:39
  • 2
    Related: [Deserialization of self-referencing properties does not work](http://stackoverflow.com/q/6280890/10263) – Brian Rogers Apr 26 '16 at 19:51

1 Answers1

5

You need to use the same settings for deserialization as you did for serialization. That being said, you appear to have encountered a bug or limitation in Json.NET.

It is happening for the following reason. If your Middle type does not have a public parameterless constructor, but does have a single public constructor with parameters, JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters() will call that constructor, matching the constructor arguments to the JSON properties by name and using default values for missing properties. Then afterwards any remaining unused JSON properties will be set into the type. This enables deserialization of read-only properties. E.g. if I add a read-only property Foo to your Middle class:

public class Middle
{
    readonly int foo;

    public int Foo { get { return foo; } }

    public Middle(int Foo) { this.foo = Foo; "Middle".Dump(); }

    public Root Root { get; set; }

    public Child Child { get; set; }
}

The value of Foo will be successfully deserialized. (The matching of JSON property names to constructor argument names is shown here in the documentation, but not well explained.)

However, it appears this functionality interferes with PreserveReferencesHandling.All. Since CreateObjectUsingCreatorWithParameters() fully deserializes all child objects of the object being constructed in order to pass those necessary into its constructor, if a child object has a "$ref" to it, that reference will not be resolved, since the object will not have been constructed yet.

As a workaround, you could add a private constructor to your Middle type and set ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor:

public class Middle
{
    private Middle() { "Middle".Dump(); }

    public Middle(int Foo) { "Middle".Dump(); }

    public Root Root { get; set; }

    public Child Child { get; set; }
}

And then:

var settings = new JsonSerializerSettings
{
    Formatting = Newtonsoft.Json.Formatting.Indented,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
    PreserveReferencesHandling = PreserveReferencesHandling.All,
    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
};
var deserialized = JsonConvert.DeserializeObject<Root>(json, settings);

Of course, if you do this, you loose the ability to deserialize read-only properties of Middle, if there are any.

You might want to report an issue about this. In theory, at the expense of higher memory usage, when deserializing a type with a parameterized constructor, Json.NET could:

  • Load all child JSON properties into an intermediate JToken.
  • Only deserialize those required as constructor arguments.
  • Construct the object.
  • Add the object to the JsonSerializer.ReferenceResolver.
  • Deserialize and set the remaining properties.

However, if any of the constructor arguments thenselves have a "$ref" to the object being deserialized, this doesn't appear easily fixable.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thank you for the great explanation of why this is happening. Yeah, it would make sense if the constructor was chosen before the deserialization of the children (based on name + type) and then only the required children would be deserialized before construction - though there may be some overhead. I will report an issue and maybe a pull request. Much appreciated. – Andrew Hanlon Apr 26 '16 at 17:33