.NET 8 Preview 1 中 SystemTextJson 的改进

Intro

System.Text.Json 是从 .NET Core 3.0 开始的一个新的 JSON 处理库,在之后的版本中一直在完善和改善性能,在 .NET 8 Preview 1 中完善一些支持,具体更新如下

Improvements

Unmapped Json Property Handling

在之前的版本中,如果 json 里 property 是不希望的内容不会有任何处理,在新版本中增加了没有 mapping 的 json property 处理,可以在找不到 mapping 的时候报错,示例如下:.

file record Person(int Id, string Name);

var personJsonWithoutId = JsonSerializer.Serialize(new { Id = 1, Name = "1234", Age = 10 });

try
{
    var p = JsonSerializer.Deserialize<Person>(personJsonWithoutId);
    Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
    Console.WriteLine(e);
}

不指定没有 mapping 的 JSON property 的时候默认是允许的,以上就会正常输出,不会走到 exception,输出如下:

Person { Id = 1, Name = 1234 }

当我们指定了要报错的时候就会抛异常

try
{
    var p = JsonSerializer.Deserialize<Person>(personJsonWithoutId,
        new JsonSerializerOptions() { 
            UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow 
        });
    Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
    Console.WriteLine(e);
}

输出结果如下:

System.Text.Json.JsonException: The JSON property 'Net8Sample.<>FE3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855__Person' could not be mapped to any .NET member contained in type 'Age'.
   at System.Text.Json.ThrowHelper.ThrowJsonException_UnmappedJsonProperty(Type type, String unmappedPropertyName)
   at System.Text.Json.JsonSerializer.LookupProperty(Object obj, ReadOnlySpan`1 unescapedPropertyName, ReadStack& state, JsonSerializerOptions options, Boolean& useExtensionProperty, Boolean createExtensionProperty)
   at System.Text.Json.Serialization.Converters.ObjectWithParameterizedConstructorConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Net8Sample.JsonSample.MissingMemberHandlingTest()

除了指定 JsonSerializerOptions 我们也可以针对某一个类型添加 JsonUnmappedMemberHandling 标记,示例如下:

[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)]
file record Person2
{
    public required int Id { get; init; }
    public required string Name { get; init; }
    public string? JobTitle { get; set; }
}

try
{
    var p = JsonSerializer.Deserialize<Person2>(personJsonWithoutId);
    Console.WriteLine(p?.ToString());
}
catch (Exception e)
{
    Console.WriteLine(e);
}

输出结果和前面的示例类似:

System.Text.Json.JsonException: The JSON property 'Net8Sample.<>FE3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855__Person2' could not be mapped to any .NET member contained in type 'Age'.
   at System.Text.Json.ThrowHelper.ThrowJsonException_UnmappedJsonProperty(Type type, String unmappedPropertyName)
   at System.Text.Json.JsonSerializer.LookupProperty(Object obj, ReadOnlySpan`1 unescapedPropertyName, ReadStack& state, JsonSerializerOptions options, Boolean& useExtensionProperty, Boolean createExtensionProperty)
   at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.TryRead(Utf8JsonReader& reader, Type typeToConvert, JsonSerializerOptions options, ReadStack& state, T& value)
   at System.Text.Json.Serialization.JsonConverter`1.ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
   at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Deserialize(Utf8JsonReader& reader, ReadStack& state)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 utf8Json, JsonTypeInfo`1 jsonTypeInfo, Nullable`1 actualByteCount)
   at System.Text.Json.JsonSerializer.ReadFromSpan[TValue](ReadOnlySpan`1 json, JsonTypeInfo`1 jsonTypeInfo)
   at System.Text.Json.JsonSerializer.Deserialize[TValue](String json, JsonSerializerOptions options)
   at Net8Sample.JsonSample.MissingMemberHandlingTest()

在 System.Text.Json 中有个特殊的特性,我们可以使用 JsonExtensionData 来匹配那些没有 mapping 的 JSON property,那两个一起使用会不会报错呢,我们也来试一下

file record PersonWithExtensionData
{
    public required int Id { get; init; }
    public required string Name { get; init; }
    [JsonExtensionData]
    public Dictionary<string,object>? Extensions { get; set; }
}

try
{
    var p = JsonSerializer.Deserialize<PersonWithExtensionData>(personJsonWithoutId,
        new JsonSerializerOptions() { UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow });
    Console.WriteLine(JsonSerializer.Serialize(p));
}
catch (Exception e)
{
    Console.WriteLine(e);
}

输出结果如下:

{"Id":1,"Name":"1234","Age":10}

是否和你猜测的一致呢

Interface Hierarchy

在之前的版本中如果我们使用接口进行序列化的话,接口继承的属性是不会被序列化的,比如下面的代码:

file interface IBase
{
    int Base { get; set; }
}
file interface IDerived : IBase
{
    int Derived { get; set; }
}
file class DerivedImplement : IDerived
{
    public int Base { get; set; }
    public int Derived { get; set; }
}

IDerived value = new DerivedImplement() { Base = 0, Derived =1 };
var serializedValue = JsonSerializer.Serialize(value);
Console.WriteLine(serializedValue);

在 .NET 7 中输出结果如下:

.NET 8 Preview 1 中 SystemTextJson 的改进

.NET 7 interface serialize output

在 .NET 8 Preview 1 输出结果如下:

.NET 8 Preview 1 中 SystemTextJson 的改进

.NET 8 Preview interface serialize output

SnakeCaseNaming && KebabCaseNaming

在 .NET 8 Preview 1 中新增了两种属性名称序列化方式,SnakeCase 和 KebabCase,两种方式分别有 大写形式和小写形式,使用的时候在 JsonSerializerOptions 中指定 PropertyNamingPolicy 即可,我们直接看下示例吧

private static void SnakeCaseNamingTest()
{
    var p = new Person2() 
    { 
        Id = 1, 
        Name = "Alice",
        JobTitle = "Engineer" 
    };
    var snakeCaseLowerJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
    });
    Console.WriteLine(snakeCaseLowerJson);

    var snakeCaseUpperJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseUpper
    });
    Console.WriteLine(snakeCaseUpperJson);
}

输出结果如下:

.NET 8 Preview 1 中 SystemTextJson 的改进

SnakeCaseNaming output

再来看下 KebabCase 的示例:

private static void KebabCaseNamingTest()
{
    var p = new Person2() 
    { 
        Id = 1, 
        Name = "Alice",
        JobTitle = "Engineer" 
    };
    var kebabCaseLowerJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
    {
        PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower
    });
    Console.WriteLine(kebabCaseLowerJson);

    var kebabCaseUpperJson = JsonSerializer.Serialize(p, new JsonSerializerOptions()
    {
        PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper
    });
    Console.WriteLine(kebabCaseUpperJson);
}

输出结果如下:

.NET 8 Preview 1 中 SystemTextJson 的改进

KebabCaseNaming output

JsonSerializerOptions-ReadOnly

JsonSerializerOptions 中增加了 IsReadOnly 和 MakeReadOnly 两个方法,我们可以在为某个类型的序列化指定了某些序列化选项之后调用 MakeReadOnly 方法来保证序列化选项不会再被修改来保证序列化行为的一致性,下面是一个示例:

private static void JsonSerializerOptionsReadOnlyTest()
{
    var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
    {
        TypeInfoResolver = new DefaultJsonTypeInfoResolver()
    };
    Console.WriteLine($"IsReadOnly: {options.IsReadOnly}");
    options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    Console.WriteLine("PropertyNamingPolicy updated");

    options.MakeReadOnly();
    Console.WriteLine($"IsReadOnly: {options.IsReadOnly}");

    try
    {
        options.PropertyNamingPolicy = null;
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}

输出结果如下:

.NET 8 Preview 1 中 SystemTextJson 的改进

从上面的输出可以看得出来,在我们调用 MakeReadOnly 方法之前 IsReadOnly 会是 false,是可以修改 options 的配置的,在调用之后 IsReadOnly 就变成 true 了,再修改 options 的配置就会抛异常

More

细心的小伙伴可能会发现第一个示例 Unmapped Json Property Handling 部分示例的异常信息是有点问题的,property 和 type 信息的位置反了,这是一个 BUG 。。,目前 bug 已经修复了,preview 2 应该就没这个问题了,修复 PR 可以参考:https://github.com/dotnet/runtime/pull/81718