从 .NET 9 开始,JsonSerializer 在序列化和反序列化中都(有限地)支持不可为 null 的引用类型强制实施。 可以使用 JsonSerializerOptions.RespectNullableAnnotations 标志切换此支持。
例如,以下代码片段在序列化过程中引发 JsonException,并显示如下消息:
类型“Person”的属性或字段“Name”不允许获取 null 值。 请考虑更新其可为 Null 性批注。
public static void RunIt()
{
#nullable enable
JsonSerializerOptions options = new()
{
RespectNullableAnnotations = true
};
Person invalidValue = new(Name: null!);
JsonSerializer.Serialize(invalidValue, options);
}
record Person(string Name);
同样,RespectNullableAnnotations 对反序列化强制执行可为 Null 性。 以下代码片段在序列化过程中引发 JsonException,并显示如下消息:
类型“Person”上的构造函数参数“Name”不允许 null 值。 请考虑更新其可为 Null 性批注。
public static void RunIt()
{
#nullable enable
JsonSerializerOptions options = new()
{
RespectNullableAnnotations = true
};
string json = """{"Name":null}""";
JsonSerializer.Deserialize<Person>(json, options);
}
record Person(string Name);
提示
- 可以使用 IsGetNullable 和 IsSetNullable 属性在单个属性级别配置可为 Null 性。
- C# 编译器使用
[NotNull]、[AllowNull]、[MaybeNull]和[DisallowNull]属性在 getter 和 setter 中微调批注。 这些属性也可以通过此 System.Text.Json 功能识别。 (有关属性的详细信息,请参阅 null 状态静态分析的属性。)
限制
由于不可为 null 引用类型的实现方式,此功能存在一些重要限制。 在启用该功能之前,请熟悉这些限制。 问题的根是引用类型可为 Null 性在中间语言 (IL) 中没有一流的表示形式。 因此,表达式 MyPoco 和 MyPoco? 从运行时反射的角度来看是不可区分的。 虽然编译器试图通过发出属性元数据来弥补这一点(请参阅 sharplab.io 示例),但此元数据仅限于特定类型定义范围内的非通用成员注释。 此限制是因为该标志仅验证存在于非通用属性、字段和构造函数参数上的可为 Null 性注释。
System.Text.Json 不支持对以下对象强制执行可为 Null 性:
- 顶级类型,或在进行第一次
JsonSerializer.Deserialize()或JsonSerializer.Serialize()调用时传递的类型。 - 集合元素类型,例如,
List<string>和List<string?>类型无法区分。 - 任何通用属性、字段或构造函数参数。
如果要在这些情况下添加可为 null 性强制,要么将你的类型建模为结构(因为它们不允许 null 值),要么编写一个自定义转换器,将其 HandleNull 属性重写为 true。
功能开关
可以使用 RespectNullableAnnotations 功能开关全局打开 System.Text.Json.Serialization.RespectNullableAnnotationsDefault 设置。 将以下 MSBuild 项添加到项目文件(例如,.csproj 文件):
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Text.Json.Serialization.RespectNullableAnnotationsDefault" Value="true" />
</ItemGroup>
RespectNullableAnnotationsDefault API 在 .NET 9 中作为选择加入标志实现,以避免中断现有应用程序。 如果要编写新应用程序,强烈建议在代码中启用此标志。
可以为 Null 参数和可选参数之间的关系
RespectNullableAnnotations 不会将强制措施扩展到未指定的 JSON 值,因为 System.Text.Json 将必需和不可为 null 的属性视为正交概念。 例如,以下代码段在反序列化过程中不会引发异常:
public static void RunIt()
{
JsonSerializerOptions options = new()
{
RespectNullableAnnotations = true
};
var result = JsonSerializer.Deserialize<MyPoco>("{}", options);
Console.WriteLine(result.Name is null); // True.
}
class MyPoco
{
public string Name { get; set; }
}
此行为源于 C# 语言本身,你可以在其中具有可以为 Null 的必需属性:
MyPoco poco = new() { Value = null }; // No compiler warnings.
class MyPoco
{
public required string? Value { get; set; }
}
还可以具有不可为 Null 的可选属性:
class MyPoco
{
public string Value { get; set; } = "default";
}
同样的正交性也适用于构造函数参数:
record MyPoco(
string RequiredNonNullable,
string? RequiredNullable,
string OptionalNonNullable = "default",
string? OptionalNullable = "default"
);
缺失值与空值
请务必了解在设置时,如何区分null和具有显式值的属性。 JavaScript 区分 undefined (缺少属性)和 null (显式 null 值)。 但是,.NET 没有 undefined 概念,所以在这两种情况下,在 .NET 中都反序列化为 null。
在反序列化期间,当 RespectNullableAnnotations 为 true 时:
显式 null 值会引发异常,针对那些不允许为空的属性。 例如,当反序列化到非 null 的
{"Name":null}属性时,string Name会引发异常。缺少的属性不会引发异常,即使对于不可为 null 的属性也是如此。 例如,
{}反序列化为非空string Name属性时不会引发异常。 序列化程序不设置属性,将其保留为构造函数的默认值。 对于未初始化的非可以为 null 的引用类型,这会导致null触发编译器警告。以下代码演示如何在反序列化期间缺少的属性不引发异常:
public static void RunIt() { #nullable enable JsonSerializerOptions options = new() { RespectNullableAnnotations = true }; // Missing property - does NOT throw an exception. string jsonMissing = """{}"""; var resultMissing = JsonSerializer.Deserialize<Person>(jsonMissing, options); Console.WriteLine(resultMissing.Name is null); // True. } record Person(string Name);
发生此行为差异的原因是缺少的属性被视为可选(未提供),而显式 null 值被视为违反不可为 null 约束的已提供值。 如果需要强制属性必须存在于 JSON 中,请使用 required 修饰符或使用 JsonRequiredAttribute 或协定模型将属性设置为必需。