使用非默认构造函数会破坏Json.net中反序列化的顺序

使用Json.net使用父子关系反序列化对象图时,非默认构造函数的使用会破坏反序列化的顺序,以便子对象在其父对象之前被反序列化(构造和属性分配),从而导致空引用。

从实验看来,所有非默认构造函数对象似乎只在所有默认构造函数对象之后实例化,奇怪的是它看起来与序列化的顺序相反(父项之前的子项)。

这会导致应该引用其父项(并且已正确序列化)的“子”对象反而使用空值反序列化。

这似乎是一种非常常见的情况,所以我想知道我是否错过了什么?

是否有更改此行为的设置? 在某种程度上它是否适用于其他场景? 除了全面创建默认构造函数之外,还有解决方法吗?

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(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输出:

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

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

 Root Child Middle Child.Middle = null 

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

 Root Middle Child Child.Middle = Middle 

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

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

 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; } } 

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

但是,此function似乎会干扰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; } } 

然后:

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

当然,如果你这样做,你就失去了对Middle只读属性进行反序列化的能力,如果有的话。

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

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

但是,如果任何构造函数参数本身对要反序列化的对象具有"$ref" ,则这似乎不容易修复。