System.Text.Json 默认不支持混合类型字段,需自定义 JsonConverter<string> 处理 string/int 混合的 Id 字段;核心是重写 Read 方法,根据 token 类型分别调用 GetString() 或 TryGetInt64()/GetDouble() 转换,Write 保持字符串输出;推荐属性级标注注册,避免全局误伤。

System.Text.Json 默认不支持混合类型字段(如 Id 既可能是 int 又可能是 string),必须靠自定义 JsonConverter<T> 解决,否则直接抛 JsonException。
为什么默认反序列化会失败
当 JSON 中某个字段在不同实例里类型不一致(例如 {"Id": 1} 和 {"Id": "abc"}),JsonSerializer.Deserialize<T> 在遇到类型不匹配时不会自动降级或尝试转换 —— 它严格按目标属性类型读取。比如声明为 string Id,但 JSON 给的是数字 token,Utf8JsonReader.GetString() 就会返回 null 或抛异常(取决于 JsonSerializerOptions.ReadCommentHandling 等配置)。
常见错误信息是:Cannot convert value to type 'System.String' 或更底层的 InvalidOperationException: Cannot get the value of a token type 'Number' as a string.
- 不是 .NET 版本问题:.NET 5+ 的
JsonNumberHandling.AllowReadingFromString只对数字类型(如int、double)启用字符串兼容,不反向生效 - 不是配置遗漏:加
PropertyNameCaseInsensitive = true或DefaultIgnoreCondition都无法绕过类型校验 - 真正卡点在于
Utf8JsonReader的 token 类型和目标类型的硬匹配逻辑
写一个能处理 string/int 混合的 Converter
核心是重写 Read 方法,在读取时主动判断 token 类型并做适配转换。下面这个 StringOrIntConverter 是生产可用的最小实现:
public class StringOrIntConverter : JsonConverter<string>
{
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => reader.GetString() ?? string.Empty,
JsonTokenType.Number => reader.TryGetInt64(out var i) ? i.ToString() : reader.GetDouble().ToString(),
_ => throw new JsonException($"Unexpected token type {reader.TokenType} for string conversion")
};
}
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
{
writer.WriteStringValue(value);
}
}
- 别只用
GetInt32():JSON 数字可能超出int范围,用TryGetInt64()更安全;若还可能含小数,补GetDouble() -
GetString()返回null是合法行为(比如 JSON 里是"Id": null),需判空避免 NRE -
Write方法保持简单即可,输出始终是字符串,无需还原原始类型
两种注册方式的区别和选型建议
自定义 converter 可以通过特性或全局选项注册,二者行为差异明显:
- 属性级(
[JsonConverter(typeof(StringOrIntConverter))]):仅对该字段生效,适合局部、异构场景;但无法复用到同类型其他字段,除非每个都加 - 全局注册(
options.Converters.Add(new StringOrIntConverter())):对所有string字段生效 —— 这通常不是你想要的,容易误伤正常字符串字段 - 更稳妥的做法是注册「泛型 converter 工厂」:比如只对特定命名(如
Id)或特定泛型约束(如class T : IIdentity)触发转换,但这需要继承JsonConverterFactory
绝大多数情况推荐属性级标注,清晰、可控、无副作用。
DateTime 格式定制这类常见需求怎么搞
如果只是想把 DateTime 输出成 yyyy-MM-dd,不要自己从头写 Read/Write —— 直接用内置的 JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()) 思路不适用,但可以复用 DateTime 的格式化能力:
public class ShortDateConverter : JsonConverter<DateTime>
{
private readonly string _format = "yyyy-MM-dd";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DateTime.ParseExact(reader.GetString()!, _format, null);
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToString(_format));
}
- 注意:输入 JSON 必须严格匹配该格式,否则
ParseExact抛异常;如需容错,改用DateTime.TryParseExact并设默认值 - 别在
Write里调用ToString("yyyy-MM-dd")后再用WriteStringValue—— 这没问题;但别用WriteNumberValue,那会输出时间戳 - 这种 converter 一旦注册到全局,所有
DateTime都会被格式化,务必确认这是全局策略
真正容易被忽略的是:converter 实例是共享的,不能在 Read 里缓存状态(比如用字段存上次解析结果),也不要在里面 new 大对象 —— 它会被高频复用,影响性能和线程安全。


















