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.