问题描述
使用 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?
是否有更改此行为的设置?它是否以某种方式设计用于其他场景?除了全面创建默认构造函数之外,还有其他解决方法吗?
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?
使用 LINQPad 或 DotNetFiddle 的简单示例:
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 输出:
{
"$id": "1",
"$type": "Root",
"Middle": {
"$id": "2",
"$type": "Middle",
"Root": {
"$ref": "1"
},
"Child": {
"$id": "3",
"$type": "Child",
"Middle": {
"$ref": "2"
}
}
}
}
Middle 的输出具有非默认构造函数:
Output with Middle having non-default constructor:
Root
Child
Middle
Child.Middle = null
Middle 的输出具有默认构造函数:
Output with Middle having default constructor:
Root
Middle
Child
Child.Middle = Middle
推荐答案
您需要使用与序列化相同的设置进行反序列化.话虽如此,您似乎在 Json.NET 中遇到了错误或限制.
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.
发生这种情况的原因如下.如果您的 Middle
类型没有公共无参数构造函数,但有一个带参数的公共构造函数,JsonSerializerInternalReader.CreateObjectUsingCreatorWithParameters()
将调用该构造函数,按名称将构造函数参数与 JSON 属性匹配,并对缺失的属性使用默认值.然后,任何剩余的未使用的 JSON 属性都将设置为类型.这启用了只读属性的反序列化.例如.如果我将只读属性 Foo
添加到您的 Middle
类:
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; }
}
Foo
的值将被成功反序列化.(JSON 属性名称与构造函数参数名称的匹配显示在 here文档,但没有得到很好的解释.)
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.)
但是,此功能似乎会干扰 PreserveReferencesHandling.All
.由于 CreateObjectUsingCreatorWithParameters()
完全反序列化正在构造的对象的所有子对象,以便将必要的那些传递给它的构造函数,如果子对象有一个 "$ref"
给它,该引用将不会被解析,因为该对象尚未被构造.
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.
作为一种解决方法,您可以将 private 构造函数添加到您的 Middle
类型并设置 ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
:
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; }
}
然后:
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
的只读属性(如果有的话)的能力.
Of course, if you do this, you loose the ability to deserialize read-only properties of Middle
, if there are any.
您可能希望报告有关此问题的问题.理论上,以更高的内存使用为代价,在使用参数化构造函数反序列化类型时,Json.NET 可以:
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:
- 将所有子 JSON 属性加载到中间
JToken
中. - 仅反序列化那些需要作为构造函数参数的参数.
- 构造对象.
- 将对象添加到
JsonSerializer.ReferenceResolver
. - 反序列化并设置剩余的属性.
但是,如果任何构造函数参数本身对被反序列化的对象有一个 "$ref"
,这似乎不容易修复.
However, if any of the constructor arguments thenselves have a "$ref"
to the object being deserialized, this doesn't appear easily fixable.
这篇关于使用非默认构造函数会破坏 Json.net 中的反序列化顺序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持跟版网!