Usage of non-default constructor breaks order of deserialization in Json.net

And*_*lon 3 c# serialization json.net

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?

是否有设置可以更改此行为?它是为其他场景设计的吗?除了全面创建默认构造函数之外,还有其他解决方法吗?

LINQPad 或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;}
}
Run Code Online (Sandbox Code Playgroud)

JSON 输出:

{
  "$id": "1",
  "$type": "Root",
  "Middle": {
    "$id": "2",
    "$type": "Middle",
    "Root": {
      "$ref": "1"
    },
    "Child": {
      "$id": "3",
      "$type": "Child",
      "Middle": {
        "$ref": "2"
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Middle 具有非默认构造函数的输出:

Root
Child
Middle
Child.Middle = null
Run Code Online (Sandbox Code Playgroud)

Middle 具有默认构造函数的输出:

Root
Middle
Child
Child.Middle = Middle
Run Code Online (Sandbox Code Playgroud)

dbc*_*dbc 5

您需要使用与序列化相同的设置进行反序列化。话虽如此,您似乎遇到了 Json.NET 中的错误或限制。

它发生的原因如下。如果您的Middle类型没有公共无参数构造函数,但有一个带参数的公共构造函数,JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters() 则将调用该构造函数,按名称将构造函数参数与 JSON 属性匹配,并对缺失的属性使用默认值。然后任何剩余的未使用的 JSON 属性将被设置到类型中。这可以反序列化只读属性。例如,如果我Foo向您的Middle班级添加只读属性:

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; }
}
Run Code Online (Sandbox Code Playgroud)

的值Foo将被成功反序列化。(JSON属性名构造参数名的匹配显示文档中,但没有得到很好的解释。)

但是,此功能似乎会干扰PreserveReferencesHandling.All. 由于CreateObjectUsingCreatorWithParameters()完全反序列化正在构造的对象的所有子对象,以便将那些必需的子对象传递给其构造函数,如果子对象具有"$ref"对它的引用,则不会解析该引用,因为尚未构造对象。

作为一种解决方法,您可以向您的类型添加一个私有构造函数Middle并设置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; }
}
Run Code Online (Sandbox Code Playgroud)

进而:

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);
Run Code Online (Sandbox Code Playgroud)

当然,如果您这样做,您将失去反序列化 的只读属性(Middle如果有)的能力。

您可能想报告有关此问题的问题。理论上,以更高的内存使用为代价,当使用参数化构造函数反序列化类型时,Json.NET 可以:

  • 将所有子 JSON 属性加载到中间JToken.
  • 仅反序列化那些需要作为构造函数参数的参数。
  • 构造对象。
  • 将对象添加到JsonSerializer.ReferenceResolver.
  • 反序列化并设置其余属性。

但是,如果任何构造函数参数本身都有一个"$ref"指向被反序列化的对象,这似乎不容易修复。