从 .NET 11 开始,JsonSerializer支持 C# 15 联合类型。 联合在其声明中保存其中一种事例类型。
JsonSerializer 写入当前 case 值,并可将其读回。
对联合类型值进行序列化和反序列化
声明一个联合类型,其各个分支具有不同的 JSON 标记类型:
public union Payload(int, string, Message);
public sealed record Message(string Text);
使用 JsonSerializer 对联合类型进行序列化和反序列化:
Payload payload = new Message("Ready");
string json = JsonSerializer.Serialize(payload);
Payload copy = JsonSerializer.Deserialize<Payload>(json);
序列化的 JSON 包含活动案例值,而不是包装器或鉴别器:
{"Text":"Ready"}
默认情况下,序列化程序按令牌类型对传入的 JSON 进行分类。 在前面的联合中,JSON 数字选择 int、JSON 字符串选择 string,JSON 对象选择 Message。
通过 JsonSerializerOptions.Web,还可以从 JSON 字符串中读取 int。 随后,Payload 的 int 和 string 两种情况都归为字符串令牌类型。 甚至在序列化程序解析任一种情况之前,JsonException 也会因歧义而引发 "25%"。 若要使用 Web 默认值读取字符串,请提供自定义分类器来选择案例。
区分具有相同 JSON 令牌类型的事例
令牌分类不能区分两种序列化为 JSON 对象的情况。 应用 JsonUnionAttribute 并选择 JsonUnionTypeStructuralClassifier 按对象事例的属性名称对对象事例进行分类:
[JsonUnion(TypeClassifier = typeof(JsonUnionTypeStructuralClassifier))]
public union Pet(Dog, Cat);
public sealed record Dog(string Name, string Breed);
public sealed record Cat(string Name, int Lives);
在这里,JsonUnionAttribute 为现有的 Pet 联合类型选择一个分类器;将其应用于普通类型并不会把该类型变成联合类型。
当有效负载包含 Lives 时,分类器选择 Dog;当其包含 Breed 时,选择 Cat:
Pet pet = JsonSerializer.Deserialize<Pet>(
"""{"Name":"Rex","Breed":"Husky"}""");
对于 JSON 对象,结构分类器以兼容的对象事例开头,并在读取识别的根级别属性名称时缩小设置的范围。 必需的属性会在案例不存在时将其删除。 JsonUnmappedMemberHandling.Disallow 会在有效负载包含案例未声明的属性时移除该案例。 仅当保留一个事例时,分类才会成功。
结构分类器不会检查属性值、嵌套对象、字符串内容或数组元素。 请记住以下后果:
- 产生零个或多个候选项的有效负载会抛出 JsonException。
- 创建分类器后,可能会拒绝重叠或阴影对象协定。
- 不支持使用相同 JSON 令牌类型的多个非对象事例。 例如,
Guid和string都使用 JSON 字符串。 - 普通对象情形不能与字典、
JsonObject或其他非 POCO 的对象形状情形混用。 - 不支持嵌套联合事例和多态事例。
- 不支持 ReferenceHandler.Preserve。
- 无法区分大小写的配置在序列化程序构建分类器时会抛出 NotSupportedException。
选择联合类型或封闭层次结构
当你需要保留并非由你控制的无判别字段的 JSON 格式,或者各个情况具有不同的 JSON 结构时,请使用联合类型。 例如,Payload 的 int 和 string 这两种情况可以通过 JSON 令牌类型加以区分。 但是,对于对象事例(例如 Pet(Dog, Cat),对属性名称的更改)可能会影响结构分类器选择哪种情况。
当你控制类型和 JSON 契约时,带有推断多态性的封闭层次结构则可以改用鉴别器来识别对象情况:
[JsonPolymorphic(InferClosedTypePolymorphism = true)]
public closed record Event;
public sealed record Created(int Id) : Event;
public sealed record Deleted(int Id) : Event;
JsonSerializer.Serialize<Event>(new Created(42))写入{"$type":"Created","Id":42}。 这两种派生类型都声明 Id,但鉴别器会独立于其属性来标识事例。 这使得案例选择随着属性的发展而更加稳定。 与联合案例不同,派生的类型必须共享基类,并且你必须选择推断的多态性;单独的 closed 修饰符不会添加鉴别器。
提供自定义分类器
当默认的令牌分类或内置的 JsonUnionTypeStructuralClassifier 无法满足您的要求时,请从 JsonTypeClassifierFactory 派生。 自定义分类器可以使用其他结构规则来选择联合事例。 在以下位置之一注册工厂:
- 自定义合同时,为 JsonTypeInfo.TypeClassifier 指定一位委托人。
- 为一个联合设置 JsonUnionAttribute.TypeClassifier。
- 将工厂添加到 JsonSerializerOptions.TypeClassifiers 中以进行基于反射的序列化。
- 为源生成上下文设置 JsonSourceGenerationOptionsAttribute.TypeClassifiers 。
分类器读取当前 JSON 值,并从中返回其中一种事例类型 JsonTypeClassifierContext.UnionCases。 序列化程序首先检查合同委托,然后检查每个联合工厂、选项级工厂和内置令牌分类。
对于不明确的联合,源生成器会报告诊断,除非在生成时配置分类器。
处理 null 值和默认联合值
联合可以声明可以为 null 的案例类型。 JSON null 选择第一个可以为 null 的情况。 如果联合没有可以为 null 的情况,JSON null 将生成默认联合值。 对于编译器生成的结构体联合,默认值没有活动 case,并会序列化为 JSON null。
自定义联合协定
对于高级场景,请通过 JsonTypeInfo 自定义联合元数据。 联合的 JsonTypeInfo.Kind 值为 JsonTypeInfoKind.Union。 其契约公开了:
- JsonTypeInfo.UnionCases,其中包含 JsonUnionCaseInfo 条目。
- JsonTypeInfo.UnionConstructor,它根据案例类型和值创建联合。
- JsonTypeInfo.UnionDeconstructor,它返回活动的案例类型和值。
- JsonTypeInfo.TypeClassifier,它在反序列化期间选择案例。
有关修改 JsonTypeInfo的详细信息,请参阅 自定义 JSON 协定。