使用非默认构造函数会破坏Json.net中

时间:2016-04-26 13:18:40

标签: c# serialization json.net

使用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

具有默认构造函数的中间输出:

Root
Middle
Child
Child.Middle = Middle

1 个答案:

答案 0 :(得分:2)

您需要使用与序列化相同的反序列化设置。话虽这么说,您似乎遇到了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",则该引用将无法解析,因为该对象还没有建成。

作为一种变通方法,您可以在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",那么这似乎不容易修复。