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


使用 Json.net 反序列化具有父子

关系的对象图时,使用非默认构造函数会中断反序列化的顺序,以便子对象在其父对象之前反序列化(构造和分配属性),从而导致 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",则这似乎不容易修复。

相关内容

  • 没有找到相关文章

最新更新