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

3
使用Json.net反序列化具有父子关系的对象图时,使用非默认构造函数会破坏反序列化顺序,导致子对象在其父对象之前进行反序列化(构造和属性分配),从而导致空引用。
经过实验,所有非默认构造函数对象似乎仅在所有默认构造函数对象之后实例化,而且奇怪的是,它们似乎按照序列化的相反顺序(先子后父)进行。
这会导致“子”对象应该引用其父对象(并正确序列化)的情况下,却被反序列化为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

@JonSkeet 谢谢,已添加! - Andrew Hanlon
1
你正在使用哪个版本的Json.NET? - dbc
@dbc 它出现在最新的NuGet包(8.0.3)中,我已经测试了所有的回溯到5.0.8,发现同样的问题 - 这让我认为这是设计上的问题(尽管我不理解)。 - Andrew Hanlon
1
7.0.1版本的发布说明中包含一条注释修复 - 修复了只读属性保留对象引用的问题,但是这个问题在该版本或之后的版本中仍未修复。 - dbc
1个回答

5
您需要使用与序列化相同的设置进行反序列化。尽管如此,您似乎遇到了Json.NET中的一个错误或限制。
这是由于以下原因。如果您的中间类型没有公共无参构造函数,但具有带参数的单个公共构造函数,则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属性名与构造函数参数名的匹配在此处文档中有展示,但解释不够清楚。)
然而,似乎这个功能会干扰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",那么这个问题似乎不容易解决。


非常感谢您对为什么会发生这种情况进行了详细的解释。如果在构造函数之前选择反序列化子项(基于名称+类型),然后只反序列化所需的子项以进行构造,那么这将是有意义的 - 尽管可能会有一些开销。我将报告一个问题,也许会提出一个拉取请求。非常感谢。 - Andrew Hanlon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接