System.Text.Json 中的 JsonExtensionData
Intro
最近两天在排查我们 API 的一个问题,查看源码过程中发现 System.Text.Json
里有一个有意思的 JsonExtensionData
在反序列化的时候,如果反序列化的 Model 中没有对应的属性信息,这些信息就会丢失,只会保留 Model 里有的数据,而 JsonExtensionData
则可以将这些没有对应属性的信息也保留下来,在序列化的时候也会保留下来。
Sample
直接来看示例吧:
定义的 Model 如下,这里使用了 C# 9 引入的 record
来简化代码
public record Person(string Name, int Age);
如果我们的 JSON 字符串正好只有这两个属性的话
JsonSerializer.Serialize(new{Name = "Ming",Age = 10,});
如果是这样的,那么也不会有什么问题
如果 JSON 字符串会有更多的信息,比如:
JsonSerializer.Serialize(new{Name = "Ming",Age = 10,Title = "SDE",City = "Shanghai"});
可以看到,这个 JSON 会有更多的信息,会包含 Model 里没有定义的 City
和 Title
此时在使用上面的 Model 就会出现信息丢失,Title
和 City
的信息就会丢掉了,System.Text.Json
提供了一种方式 JsonExtensionData
来保存这些在 Model 里没有定义的属性/字段信息
使用 JsonExtensionData
的属性/字段有类型要求,需要是以下三种类型之一:
IDictionary<string, object>
IDictionary<string, JsonElement>
JsonObject
(.NET 6 新增支持)
于是我们就有了下面的测试 Model
public record Person(string Name, int Age);public record Person1(string Name, int Age) : Person(Name, Age)
{[JsonExtensionData]public Dictionary<string, object?> Extensions { get; set; } = new();
}public record Person2(string Name, int Age) : Person(Name, Age)
{[JsonExtensionData]public Dictionary<string, JsonElement> Extensions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
}public record Person3(string Name, int Age) : Person(Name, Age)
{[JsonExtensionData]public JsonObject? Extensions { get; set; }
}
测试代码如下:
var p1 = JsonSerializer.Deserialize<Person1>(jsonString);
ArgumentNullException.ThrowIfNull(p1, nameof(p1));
WriteLine(JsonSerializer.Serialize(p1.Extensions));var p2 = JsonSerializer.Deserialize<Person2>(jsonString);
ArgumentNullException.ThrowIfNull(p2, nameof(p2));
WriteLine(JsonSerializer.Serialize(p2.Extensions));var p3 = JsonSerializer.Deserialize<Person3>(jsonString);
ArgumentNullException.ThrowIfNull(p3, nameof(p3));
WriteLine(JsonSerializer.Serialize(p3.Extensions));
输出结果如下:
可以看到使用了 JsonExtensionData
之后,多余的信息也会保存下来,把 Extensions
打印一下都是一样的结果
Extensions 中保存了我们没有匹配到的信息,这样我们就可以获取到那些可能会丢失掉的数据了
我们可以把整个对象直接打印出来
using static System.Console;var p = JsonSerializer.Deserialize<Person>(jsonString);
ArgumentNullException.ThrowIfNull(p, nameof(p));
WriteLine(JsonSerializer.Serialize(p));var p1 = JsonSerializer.Deserialize<Person1>(jsonString);
ArgumentNullException.ThrowIfNull(p1, nameof(p1));
WriteLine(JsonSerializer.Serialize(p1));var p2 = JsonSerializer.Deserialize<Person2>(jsonString);
ArgumentNullException.ThrowIfNull(p2, nameof(p2));
WriteLine(JsonSerializer.Serialize(p2));var p3 = JsonSerializer.Deserialize<Person3>(jsonString);
ArgumentNullException.ThrowIfNull(p3, nameof(p3));
WriteLine(JsonSerializer.Serialize(p3));
WriteLine(new string('-', 20));
输出结果如下:
More
借助 JsonExtensionData
我们可以实现一些比较灵活的扩展,没有用过的童鞋不妨试一下
细心的童鞋可能会发现最后一个输出的结果会有一些不同,这是一个 BUG 可以参考 issue: https://github.com/dotnet/runtime/issues/60806
上述示例可以在 Github 获取 https://github.com/WeihanLi/SamplesInPractice/blob/master/JsonSample/SystemTextJsonSample/JsonExtensionDataSample.cs
References
https://github.com/WeihanLi/SamplesInPractice/blob/master/JsonSample/SystemTextJsonSample/JsonExtensionDataSample.cs
https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-handle-overflow?WT.mc_id=DT-MVP-5004222
https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonextensiondataattribute?WT.mc_id=DT-MVP-5004222
https://github.com/dotnet/runtime/issues/61080