【问题标题】:Usage of non-default constructor breaks order of deserialization in Json.net使用非默认构造函数会破坏 Json.net 中的反序列化顺序
【发布时间】:2016-04-26 13:18:39
【问题描述】:

当使用 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<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 谢谢,已添加!
  • 您使用的是哪个版本的 Json.NET?
  • @dbc 它出现在最新的 nuget 包(8.0.3)中,我已经在 5.0.8 上对它进行了同样的测试 - 这让我认为这是设计使然(虽然我不明白)。
  • release notes for 7.0.1 包含一个注释修复 - 修复了具有只读属性的保留对象引用 -- 但在该版本或更高版本中并未修复它。

标签: c# serialization json.net


【解决方案1】:

您需要使用与序列化相同的设置进行反序列化。话虽如此,您似乎在 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 属性名称与构造函数参数名称的匹配在文档中显示为here,但没有很好地解释。)

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

作为一种解决方法,您可以将 private 构造函数添加到您的 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 的只读属性(如果有的话)的能力。

您可能想report an issue 关于此。理论上,以更高的内存使用为代价,使用参数化构造函数反序列化类型时,Json.NET 可以:

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

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

【讨论】:

  • 感谢您对为什么会发生这种情况的精彩解释。是的,如果在子项的反序列化(基于名称+类型)之前选择了构造函数,然后在构造之前只对所需的子项进行反序列化,这将是有意义的——尽管可能会有一些开销。我将报告一个问题,也许还有一个拉取请求。非常感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-05-25
  • 2018-08-18
  • 1970-01-01
  • 1970-01-01
  • 2014-09-07
  • 1970-01-01
相关资源
最近更新 更多