关系的对象图时,使用非默认构造函数会中断反序列化的顺序,以便子对象在其父对象之前反序列化(构造和分配属性),从而导致 null 引用。
从实验来看,所有非默认构造函数对象都只在所有默认构造函数对象之后实例化,奇怪的是,它似乎与序列化(子项在父项之前)的顺序相反。
这会导致应具有对其父级的引用(并且正确序列化)的"子"对象改为使用 null 值反序列化。
这似乎是一个非常常见的场景,所以我想知道我是否错过了什么?
是否有更改此行为的设置?是否以某种方式设计了其他场景?除了全面创建默认构造函数之外,还有其他解决方法吗?
使用 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;}
}
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 属性都将设置为该类型。 这将启用只读属性的反序列化。 例如,如果我将只读属性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; }
}
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; }
}
然后:
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);
当然,如果你这样做,你就失去了反序列化Middle
的只读属性的能力,如果有的话。
您可能需要报告有关此问题的问题。 理论上,当使用参数化构造函数反序列化类型时,以更高的内存使用率为代价,Json.NET 可以:
- 将所有子 JSON 属性加载到中间
JToken
中。 - 仅反序列化那些需要作为构造函数参数的参数。
- 构造对象。
- 将对象添加到
JsonSerializer.ReferenceResolver
中。 - 反序列化并设置其余属性。
但是,如果任何构造函数参数对正在反序列化的对象具有"$ref"
,则这似乎不容易修复。