使用 JSON.NET 反序列化 IOrderedEnumerable<T>

Posted

技术标签:

【中文标题】使用 JSON.NET 反序列化 IOrderedEnumerable<T>【英文标题】:Deserialization of IOrderedEnumerable<T> with JSON.NET 【发布时间】:2017-01-15 04:14:48 【问题描述】:

我和我的团队在 C# 中使用 JSON.NET 反序列化时遇到了一个奇怪的行为。

我们有一个带有 IOrderedEnumerable&lt;long&gt; 的简单 ViewModel:

public class TestClass

    public IOrderedEnumerable<long> orderedDatas  get; set; 
    public string Name  get; set; 

    public TestClass(string name)
    
        this.Name = name;
        this.orderedDatas = new List<long>().OrderBy(p => p);
    

然后,我们只想将此视图模型 POST/PUT 到 API 控制器中

[HttpPost]
public IHttpActionResult Post([FromBody]TestClass test)

    return Ok(test);

使用如下所示的 json 调用此 API:


    Name: "tiit",
    "orderedDatas": [
        2,
        3,
        4
    ],

通过这个调用,我们看到构造函数没有被调用(这可以通过它不是默认构造函数来解释)。但奇怪的是,如果我们把集合的类型改成IEnumerable或者IList,构造函数就正确调用了。

如果我们将TestClass 的构造函数更改为默认构造函数:

public class TestClass

    public IOrderedEnumerable<long> orderedDatas  get; set; 
    public string Name  get; set; 

    public TestClass()
    
        this.Name = "default";
        this.orderedDatas = new List<long>().OrderBy(i => i);
    

控制器检索的对象不会为空。 如果我们将集合的类型更改为IEnumerable,并为构造函数保留一个参数(公共TestClass(string name)),它也可以工作。

另外一个奇怪的是控制器中的对象测试是“null”。不仅orderedDatas为null,整个对象也是。

如果我们在类上添加一个属性[JsonObject],并在属性orderedData 上添加一个[JsonIgnore],它就可以工作。

目前,我们将对象更改为一个简单的 List 并且它工作正常,但我们想知道为什么 JSON 反序列化的行为会根据集合的类型而有所不同。

如果我们直接使用 JsonConvert.Deserialize :

var json = " Name: 'tiit', 'orderedDatas': [2,3,4,332232] ";
var result = JsonConvert.DeserializeObject<TestClass>(json);

我们可以看到实际的异常:

无法创建和填充列表类型 System.Linq.IOrderedEnumerable`1[System.Int64]。路径 'orderedDatas', 第 1 行,位置 33。

感谢任何想法/帮助。

谢谢!

** 编辑:感谢您的回答。有一件事我一直觉得很奇怪(我用粗体表示),如果你有任何想法来解释这种行为,请告诉我**

【问题讨论】:

IOrderedEnumerable 只是一个接口。列表和数组是 not 有序的,因此它们不实现该接口。如果您想使用特定的集合类型而不是数组,您可能应该使用自定义转换器来创建您想要的类型(也许是 SortedList?) 【参考方案1】:

正如 kisu 在 this answer 中所说,Json.NET 无法反序列化您的 TestClass,因为它没有将 IOrderedEnumerable&lt;T&gt; 接口映射到反序列化所需的具体类的内置逻辑。这并不奇怪,因为:

    IOrderedEnumerable&lt;TElement&gt; 没有公开可用的属性来指示它是如何排序的——升序;下降;使用一些复杂的keySelector 委托,它引用一个或多个captured variables。因此,这些信息会在序列化过程中丢失 - 并且即使信息是公开的,也不会实现对诸如 keySelector 委托之类的委托进行序列化。

    实现此接口的具体 .Net 类 OrderedEnumerable&lt;TElement, TKey&gt;internal。通常它由Enumerable.OrderBy()Enumerable.ThenBy() 返回,而不是直接在应用程序代码中创建。有关示例实现,请参阅 here。

对您的TestClass 进行最小 更改以使其可被 Json.NET 序列化将添加 params long [] orderedDatas 到您的构造函数中:

public class TestClass

    public IOrderedEnumerable<long> orderedDatas  get; set; 
    public string Name  get; set; 

    public TestClass(string name, params long [] orderedDatas)
    
        this.Name = name;
        this.orderedDatas = orderedDatas.OrderBy(i => i);
    

这利用了这样一个事实,即当一个类型只有一个公共构造函数时,如果该构造函数被参数化,Json.NET 将调用它来构造该类型的实例,匹配和反序列化 JSON 属性到构造函数参数按名称(模数大小写)

话虽如此,我不推荐这种设计。从OrderedEnumerable&lt;TElement, TKey&gt;.GetEnumerator() 的参考源中,我们可以看到底层的可枚举被重新排序每次调用GetEnumerator()。因此,您的实施可能非常低效。当然,在往返之后排序逻辑会丢失。要明白我的意思,请考虑以下几点:

var test = new TestClass("tiit");
int factor = 1;
test.orderedDatas = new[]  1L, 6L .OrderBy(i => factor * i);
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));
factor = -1;
Console.WriteLine(JsonConvert.SerializeObject(test, Formatting.Indented));

第一次调用Console.WriteLine() 打印出来


  "orderedDatas": [
    1,
    6
  ],
  "Name": "tiit"

第二个打印出来


  "orderedDatas": [
    6,
    1
  ],
  "Name": "tiit"

如您所见,orderedDatas 每次枚举时都会根据捕获的变量factor 的当前值重新排序。 Json.NET 所能做的只是在序列化时快照当前序列,它无法序列化序列如何不断地自我重新排序的动态逻辑

示例fiddle。

当然,当您将属性更改为IList&lt;long&gt; 时,不会引发异常并且对象将被反序列化。 Json.NET 具有将接口IList&lt;T&gt;IEnumerable&lt;T&gt; 反序列化为List&lt;T&gt; 的内置逻辑。由于解释的原因,它没有用于IOrderedEnumerable&lt;T&gt; 的内置具体类型。

更新

您问,套用一下,为什么在尝试反序列化嵌套的 IOrderedEnumerable&lt;T&gt; 属性但失败时不调用参数化构造函数,而调用无参数构造函数?

Json.NET 在反序列化带有和不带参数化构造函数的对象时使用不同的操作顺序。问题Usage of non-default constructor breaks order of deserialization in Json.net 的答案中解释了这种差异。记住这个答案,Json.NET 究竟什么时候会抛出异常,因为尝试反序列化 IOrderedEnumerable&lt;T&gt; 类型的实例却失败了?

    当类型具有参数化构造函数时,Json.NET 将仔细检查 JSON 文件中的属性,并在构造对象之前对每个属性进行反序列化 ,然后将适当的值传递给构造函数。因此,当抛出异常时,不会构造对象。

    当类型具有无参数构造函数时,Json.NET 将构造一个实例并开始细化 JSON 属性以反序列化它们,在中途抛出异常。因此,当抛出异常时,对象被构造但未完全反序列化。

显然,在您的框架中的某个地方,来自 Json.NET 的异常被捕获并吞噬了。因此,在第 1 种情况下,您会得到一个 null 对象,而在第 2 种情况下,您会得到一个部分反序列化的对象。

【讨论】:

【参考方案2】:

从我在JsonArrayContract的构造函数中看到的,接口IOrderedEnumerable&lt;&gt;没有被正确处理,很可能是因为它没有被List&lt;&gt;T[]实现,因此需要一些更特殊的处理,基本上可以替换为其他接口。

此外,将无大小的接口放入反序列化的类中是没有意义的,因为根据定义,它们不能是无限的,并且可以被具有有限、已知大小的集合替换。

请记住,在反序列化对象中使用接口需要反序列化器进行一些“猜测工作”以创建正确的实例。

如果你想要一些具有指定顺序的不可变集合,我建议你使用IReadOnlyList&lt;&gt;

【讨论】:

以上是关于使用 JSON.NET 反序列化 IOrderedEnumerable<T>的主要内容,如果未能解决你的问题,请参考以下文章

使用 Json.NET 自定义反序列化

使用 Json.NET 反序列化复杂对象

使用 Json.net 反序列化 JSON 对象数组

使用来自 URL 的 JSON.NET 反序列化 JSON 数组

使用 Json.net 将 JSON 对象反序列化为动态对象

使用JSON.NET反序列化json - 无法反序列化,因为类型需要一个JSON数组c#