November 3rd, 2024

.NET 9 中System.Text.Json 的新增功能 

Mia Wu
Partner Technical Advisor

本篇翻译于Eirik Tsarpalis的 What’s new in System.Text.Json in .NET 9 – .NET Blog 

System.Text.Json9.0 版本包含许多功能,主要侧重于 JSON 架构和智能应用程序支持。它还包括一些备受期待的增强功能,例如可空引用类型支持、自定义枚举成员名称、无序元数据反序列化和自定义序列化缩进 

获取最新信息 

您可以通过引用 System.Text.Json NuGet 的最新版本或 .NET 9 的最新 SDK 来尝试新功能 

JSON 架构导出器 

新的 JsonSchemaExporter 类可以使用 JsonSerializerOptions JsonTypeInfo 实例从 .NET 类型中提取 JSON 架构文档 

using System.Text.Json.Schema;

JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person));
Console.WriteLine(schema.ToString());
//{
//  "type": ["object", "null"],
//  "properties": {
//    "Name": { "type": "string" },
//    "Age": { "type": "integer" },
//    "Address": { "type": ["string", "null"], "default": null }
//  },
//  "required": ["Name", "Age"]
//}

record Person(string Name, int Age, string? Address = null);

生成的模式为该类型提供了JSON序列化契约的规范。从这个例子中可以看出,它区分了可空属性和不可空属性,并根据构造函数参数是否可选来填充“required”关键字。模式的输出可以通过在 JsonSerializerOptions JsonTypeInfo 实例中指定的配置进行影响: 

JsonSerializerOptions options = new(JsonSerializerOptions.Default)
{
    PropertyNamingPolicy = JsonNamingPolicy.KebabCaseUpper,
    NumberHandling = JsonNumberHandling.WriteAsString,
    UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};

JsonNode schema = options.GetJsonSchemaAsNode(typeof(MyPoco));
Console.WriteLine(schema.ToString());
//{
//  "type": ["object", "null"],
//  "properties": {
//    "NUMERIC-VALUE": {
//      "type": ["string", "integer"],
//      "pattern": "^-?(?:0|[1-9]\\d*)$"
//    }
//  },
//  "additionalProperties": false
//}

class MyPoco
{
    public int NumericValue { get; init; }
}

用户可以使用 JsonSchemaExporterOptions 配置类型进一步控制生成的模式: 

JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonSchemaExporterOptions exporterOptions = new()
{
    // Marks root-level types as non-nullable
    TreatNullObliviousAsNonNullable = true,
};

JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person), exporterOptions);
Console.WriteLine(schema.ToString());
//{
//  "type": "object",
//  "properties": {
//    "Name": { "type": "string" }
//  },
//  "required": ["Name"]
//}

record Person(string Name);

最后,用户可以通过指定 TransformSchemaNode 委托, 对生成的架构节点应用自己的转换。以下是包含来自 DescriptionAttribute 注释的文本的示例 

JsonSchemaExporterOptions exporterOptions = new()
{
    TransformSchemaNode = (context, schema) =>
    {
        // Determine if a type or property and extract the relevant attribute provider
        ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
            ? context.PropertyInfo.AttributeProvider
            : context.TypeInfo.Type;

        // Look up any description attributes
        DescriptionAttribute? descriptionAttr = attributeProvider?
            .GetCustomAttributes(inherit: true)
            .Select(attr => attr as DescriptionAttribute)
            .FirstOrDefault(attr => attr is not null);

        // Apply description attribute to the generated schema
        if (descriptionAttr != null)
        {
            if (schema is not JsonObject jObj)
            {
                // Handle the case where the schema is a boolean
                JsonValueKind valueKind = schema.GetValueKind();
                Debug.Assert(valueKind is JsonValueKind.True or JsonValueKind.False);
                schema = jObj = new JsonObject();
                if (valueKind is JsonValueKind.False)
                {
                    jObj.Add("not", true);
                }
            }

            jObj.Insert(0, "description", descriptionAttr.Description);
        }

        return schema;
    }
};

综合以上内容,我们现在可以生成一个包含来自属性注释的description关键字源的模式 

JsonNode schema = options.GetJsonSchemaAsNode(typeof(Person), exporterOptions);
Console.WriteLine(schema.ToString());
//{
//  "description": "A person",
//  "type": ["object", "null"],
//  "properties": {
//    "Name": { "description": "The name of the person", "type": "string" }
//  },
//  "required": ["Name"]
//}

[Description("A person")]
record Person([property: Description("The name of the person")] string Name);

在为 .NET 方法或 API 生成架构时,这是一个特别有用的组件;它被用于支持 ASP.NET Core 最新发布的 OpenAPI 组件,并且我们已将其部署到许多具有工具调用要求的 AI 相关库和应用程序中,例如 Semantic Kernel、Visual Studio Copilot 和最新发布的 Microsoft.Extensions.AI  

流式处理多个 JSON 文档 

Utf8JsonReader 现在支持从单个缓冲区或流中读取多个以空格分隔的 JSON 文档。默认情况下,如果 Utf8JsonReader 检测到第一个顶级文档后面有任何非空格字符,它将抛出 异常。这可以使用 JsonReaderOptions.AllowMultipleValues 标志来改变 

JsonReaderOptions options = new() { AllowMultipleValues = true };
Utf8JsonReader reader = new("null {} 1 \r\n [1,2,3]"u8, options);

reader.Read();
Console.WriteLine(reader.TokenType); // Null

reader.Read();
Console.WriteLine(reader.TokenType); // StartObject
reader.Skip();

reader.Read();
Console.WriteLine(reader.TokenType); // Number

reader.Read();
Console.WriteLine(reader.TokenType); // StartArray
reader.Skip();

Console.WriteLine(reader.Read()); // False

此外,这还使得从可能包含无效 JSON 尾部数据的有效负载中读取 JSON 成为可能 

Utf8JsonReader reader = new("[1,2,3]    <NotJson/>"u8, new() { AllowMultipleValues = true });

reader.Read();
reader.Skip(); // Success
reader.Read(); // throws JsonReaderException

在流式反序列化方面,我们添加了新的 JsonSerializer.DeserializeAsyncEnumerable 重载,使流式处理多个顶级值成为可能。默认情况下,DeserializeAsyncEnumerable 将尝试流式处理顶级 JSON 数组中包含的元素。这可以使用新的 topLevelValues 标志切换 

ReadOnlySpan<byte> utf8Json = """[0] [0,1] [0,1,1] [0,1,1,2] [0,1,1,2,3]"""u8;
using var stream = new MemoryStream(utf8Json.ToArray());

await foreach (int[] item in JsonSerializer.DeserializeAsyncEnumerable<int[]>(stream, topLevelValues: true))
{
    Console.WriteLine(item.Length);
}

遵循可null注释 

JsonSerializer 现在为序列化和反序列化中的非空引用类型强制增加了有限的支持。这可以使用 RespectNullableAnnotations 标志进行切换 

#nullable enable
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };

MyPoco invalidValue = new(Name: null!);
JsonSerializer.Serialize(invalidValue, options);
// System.Text.Json.JsonException: The property or field 'Name'
// on type 'MyPoco' doesn't allow getting null values. Consider
// updating its nullability annotation. 

record MyPoco(string Name);
JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
string json = """{"Name":null}""";
JsonSerializer.Deserialize<MyPoco>(json, options);
// System.Text.Json.JsonException: The constructor parameter 'Name'
// on type 'MyPoco' doesn't allow null values. Consider updating
// its nullability annotation.

限制 

由于非空引用类型的实现方式,此功能带有一些重要的限制,用户在启用之前需要熟悉这些限制。问题的根源在于引用类型可空性在 IL 中没有一流的表示形式例如从运行时反射的角度来看,表达式 MyPoco MyPoco? 无法区分的。虽然编译器会尽可能通过发出属性元数据来弥补这一点,但这仅限于特定类型定义范围内的非泛型成员注解。正是出于这个原因,该标志仅验证非泛型属性、字段和构造函数参数上存在的可空性注释。System.Text.Json 不支持对 

  • 顶级类型,也就是进行第一次 JsonSerializer.(De)serialize 调用时传递的类型。 
  • 集合元素类型,也就是我们无法区分 List<string> List<string?> 类型。 
  • 任何通用的属性、字段或构造函数参数 

如果您希望在这些情况下强制执行可空性验证,建议您将类型建模为 struct(因为结构体不允许空值),或者编写一个自定义转换器,将其 HandleNull 属性重写为 true。 

功能开关 

用户可以使用 System.Text.Json.Serialization.RespectNullableAnnotationsDefault 功能开关全局打开 RespectNullableAnnotations 设置,该开关可通过项目配置进行设置 

<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
</ItemGroup>

可空参数和可选参数之间的关系 

需要注意的是,RespectNullableAnnotations 不会将强制执行范围扩展到未指定的 JSON  

JsonSerializerOptions options = new() { RespectNullableAnnotations = true };
var result = JsonSerializer.Deserialize<MyPoco>("{}", options); // No exception!
Console.WriteLine(result.Name is null); // True

class MyPoco
{
    public string Name { get; set; }
}

这是因为 STJ 将必需属性和非可空属性视为正交概念。这源于 C# 语言本身,在 C# 语言中,您可以拥有可空的required属性 

MyPoco poco = new() { Value = null }; // No compiler warnings

class MyPoco
{
    public required string? Value { get; set; }
}

以及不可为空的可选属性 

class MyPoco
{
    public string Value { get; set; } = "default";
}

同样的正交性也适用于构造函数参数 

record MyPoco(
    string RequiredNonNullable,
    string? RequiredNullable,
    string OptionalNonNullable = "default",
    string? OptionalNullable = "default");

遵循非可选的构造函数参数 

基于 STJ 构造函数的反序列化历来将所有构造函数参数视为可选,如以下示例中所示 

var result = JsonSerializer.Deserialize<Person>("{}");
Console.WriteLine(result); // Person { Name = , Age = 0 }

record Person(string Name, int Age);

.NET 9 中,我们包含了 RespectRequiredConstructorParameters 标志,该标志会改变行为,使得非可选的构造函数参数现在被视为必需的 

JsonSerializerOptions options = new() { RespectRequiredConstructorParameters = true };
string json = """{"Optional": "value"}""";
JsonSerializer.Deserialize<MyPoco>(json, options);

record MyPoco(string Required, string? Optional = null);
// JsonException: JSON deserialization for type 'MyPoco' 
// was missing required properties including: 'Required'.

功能开关 

用户可以使用 System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault 功能开关全局打开 RespectRequiredConstructorParameters 设置,该开关可通过项目配置进行设置 

<ItemGroup>
  <RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectRequiredConstructorParametersDefault" Value="true" />
</ItemGroup>

RespectNullableAnnotations RespectRequiredConstructorParameter 属性均作为可选标记实现,以避免破坏现有应用程序。如果您正在编写新应用程序,强烈建议您在代码中启用这两个标记 

自定义枚举成员名称 

新的 JsonStringEnumMemberName 特性可以用来为作为字符串序列化的类型中的单个枚举成员自定义名称: 

JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2); // "Value1, Custom enum value"

[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
    Value1 = 1,
    [JsonStringEnumMemberName("Custom enum value")]
    Value2 = 2,
}

无序元数据读取 

System.Text.Json 的某些功能(例如多态性或 ReferenceHandler.Preserve)需要在数据传输中发出元数据属性: 

JsonSerializerOptions options = new() { ReferenceHandler = ReferenceHandler.Preserve };
Base value = new Derived("Name");
JsonSerializer.Serialize(value, options); // {"$id":"1","$type":"derived","Name":"Name"}

[JsonDerivedType(typeof(Derived), "derived")]
record Base;
record Derived(string Name) : Base;

默认情况下,STJ元数据读取器要求元数据属性$id$type必须在JSON对象的开始处定义 

JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""");
// JsonException: The metadata property is either not supported by the
// type or is not the first property in the deserialized JSON object.

众所周知,当需要反序列化不是来自 System.Text.Json JSON 有效负载时,这会产生问题。可以配置新的 AllowOutOfOrderMetadataProperties 来禁用此限制 

JsonSerializerOptions options = new() { AllowOutOfOrderMetadataProperties = true };
JsonSerializer.Deserialize<Base>("""{"Name":"Name","$type":"derived"}""", options); // Success

启用此标志时应小心谨慎,因为在对非常大的 JSON 对象执行流式反序列化时,它可能会导致缓冲过度(和 OOM 故障)。这是因为元数据属性必须在实例化反序列化对象之前读取,这意味着所有位于 $type 属性之前的属性必须保留在缓冲区中,以便后续的属性绑定。 

自定义缩进 

JsonWriterOptions JsonSerializerOptions 类型现在公开了用于配置缩进的 API。以下示例启用了单制表符缩进 

JsonSerializerOptions options = new()
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 1,
};

JsonSerializer.Serialize(new { Value = 42 }, options);

JsonObject 属性顺序操作 

JsonObject 类型是可变 DOM 的一部分,用于表示 JSON 对象。尽管该类型被建模为 IDictionary<string, JsonNode>,但它确实封装了用户不可修改的隐式属性顺序。新版本公开了其他 API,可有效地将该类型建模为有序字典 

JsonSerializerOptions options = new()
{
    WriteIndented = true,
    IndentCharacter = '\t',
    IndentSize = 1,
};

JsonSerializer.Serialize(new { Value = 42 }, options);

这允许修改可以直接影响属性顺序的对象实例:

// Adds or moves the $id property to the start of the object
var schema = (JsonObject)JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(MyPoco));
switch (schema.IndexOf("$id", out JsonNode? idValue))
{
    case < 0: // $id property missing
       idValue = (JsonNode)"https://example.com/schema";
       schema.Insert(0, "$id", idValue);
       break;

    case 0: // $id property already at the start of the object
        break; 

    case int index: // $id exists but not at the start of the object
        schema.RemoveAt(index);
        schema.Insert(0, "$id", idValue);
}

JsonElement JsonNode 中的 DeepEquals 方法 

新的 JsonElement.DeepEquals 方法扩展了对 JsonElement 实例的深度相等比较,补充了已有的 JsonNode.DeepEquals 方法。此外,这两个方法在其实现中进行了改进,例如处理等效 JSON 数字表示的方式: 

JsonElement left = JsonDocument.Parse("10e-3").RootElement;
JsonElement right = JsonDocument.Parse("0.001").RootElement;
JsonElement.DeepEquals(left, right); // True

JsonSerializerOptions.Web 

新的 JsonSerializerOptions.Web 单例可以使用 JsonSerializerDefaults.Web 设置快速序列化值 

JsonSerializerOptions options = JsonSerializerOptions.Web; // used instead of new(JsonSerializerDefaults.Web);
JsonSerializer.Serialize(new { Value = 42 }, options); // {"value":42}

性能改进 

有关 .NET 9 System.Text.Json 性能改进的详细说明,请参阅 Stephen Toub “.NET 9 中的性能改进文章中的相关部分 

结束语 

.NET 9 拥有大量新功能和使用质量改进,重点是 JSON 架构和智能应用程序支持。在 .NET 9 开发期间,共有 46 个拉取请求 System.Text.Json 做出贡献。我们希望您尝试新功能并向我们提供反馈,告诉我们它如何改进您的应用程序,以及您可能遇到的任何可用性问题或错误 

我们随时欢迎社区贡献。如果您想为 System.Text.Json 做出贡献,请查看我们在 GitHub 上的求助问题列表 

如果您有任何技术问题,欢迎来Microsoft Q&A 提问。

Author

Mia Wu
Partner Technical Advisor

0 comments