使用非默认构造函数会破坏Json.net中反序列化的顺序 [英] Usage of non-default constructor breaks order of deserialization in Json.net

查看:60
本文介绍了使用非默认构造函数会破坏Json.net中反序列化的顺序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用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类型没有公共的无参数构造函数,但是有一个带有参数的公共构造函数,则

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属性名称与构造函数参数名称的匹配.文档,但没有得到很好的解释.)

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.

作为一种解决方法,您可以在Middle类型中添加 private 构造函数,并设置

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 .
  • 反序列化并设置其余属性.
  • Load all child JSON properties into an intermediate JToken.
  • Only deserialize those required as constructor arguments.
  • Construct the object.
  • Add the object to the JsonSerializer.ReferenceResolver.
  • Deserialize and set the remaining properties.

但是,如果任何构造函数参数本身对反序列化的对象都具有"$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中反序列化的顺序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆