.NET 导出的 JSON Schema 拒绝扩展字段,反序列化却能接受
System.Text.Json 的全局 Disallow 与 JsonExtensionData 在 schema 导出时可能不一致。对扩展数据类型显式设置 Skip,保留其他 DTO 的严格规则。
同一份 JSON,JsonSerializer.Deserialize 能读进来,未知字段也保存在扩展数据字典里,导出的 JSON Schema 却带有 additionalProperties: false。下游按 schema 验证,就会拒绝序列化器自己接受并能写回的数据。
这个组合出现在全局设置 JsonUnmappedMemberHandling.Disallow,同时某个类型声明 [JsonExtensionData] 时。已确认的版本包括 .NET 9.0.20、10.0.12 和 11 RC1;其中 10.0.12 的正式源码里,序列化器与导出器读取的有效规则不同。不要因为 JSON 本身能往返,就忽略导出的契约。
扩展数据覆盖了全局规则,导出器却没跟上
序列化器配置类型时,若存在扩展数据属性,又没有显式的类型级设置,会按 Skip 处理额外字段并交给扩展数据字典。导出器却按“类型级设置,否则全局设置”决定是否关闭额外属性,没有采用同一套扩展数据覆盖逻辑。
因此下面两件事可以同时发生:Extra 被读入并原样写回,schema 却禁止 Extra。这与真的没有扩展数据属性、运行时也应拒绝未知字段的 DTO 不同。
先比较实际导出的 schema 与对应 JsonSerializerOptions。不要拿另一份宽松 options 做往返测试,再推断严格实例也有相同行为。还要确认扩展属性类型符合要求,例如 Dictionary<string, JsonElement>,而不是只在普通字典上起了一个 ExtensionData 名字。
在允许扩展的类型上明确标注
若这类 DTO 的协议本来就允许额外字段,可明确写出类型级 Skip。下面是 .NET 9 及以上的独立示例:
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Schema;
using System.Text.Json.Serialization;
var options = new JsonSerializerOptions(JsonSerializerOptions.Default)
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow
};
var value = JsonSerializer.Deserialize<Payload>(
@"{""Id"":1,""Extra"":42}", options)!;
Console.WriteLine(value.ExtensionData["Extra"].GetInt32());
Console.WriteLine(JsonSerializer.Serialize(value, options));
Console.WriteLine(options.GetJsonSchemaAsNode(typeof(Payload)));
[JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Skip)]
public sealed class Payload
{
public int Id { get; set; }
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }
= new();
}
这里 Skip 不会把已经由 [JsonExtensionData] 接管的字段丢掉。对这个类型显式赋值,是为了让导出器也看见运行时已有的开放契约。其他没有该标注的普通 DTO 仍受全局 Disallow 约束。
整合到已有类型时,保留其属性名、转换器、命名策略与既有 resolver;不要为了复制示例另建一份缺少项目配置的 options。若类型上原本明确声明 Disallow,则应先重新确认协议意图,不能机械覆盖它。
对于必须拒绝未知字段的接口,正确方向是收紧运行时模型,不应仅为消除 schema 报错而添加开放扩展字典。也不要把整份 schema 的所有 additionalProperties 都改成 true;嵌套 DTO 可能各有不同规则。
同时验收开放类型与严格类型
用相同 options 导出 schema,再反序列化带 Extra 的对象。扩展数据应保留 42,重新序列化后仍包含它,schema 的该对象层级不应再无条件禁止额外属性。
再选一个没有扩展数据的普通 DTO,确认未知字段仍被拒绝,其导出 schema 仍保留关闭额外字段的约束。只验证开放的 Payload,会漏掉全局严格规则被意外放宽的风险。
截至 2026 年 10 月 23 日,不能只因问题已关闭就认定所用正式运行时已经修复。保留类型标注的差异与 schema 样本,采用后续正式版本前重新比较原故障组合,再决定是否撤掉临时标注。不要通过忽略下游验证失败来恢复接口。


