自定义 system.text.json.jsonconverter 需继承 jsonconverter 并重写 read/write,t 必为具体类型;注册方式影响作用域,日期类型需绕过内置逻辑;性能关键点在于避免反射、临时字符串和二次序列化。

如何为自定义类型编写 System.Text.Json.JsonConverter
直接继承 JsonConverter<t></t>,重写 Read 和 Write 方法即可。关键不是“能不能写”,而是“怎么让序列化行为符合业务语义”。比如枚举用字符串名序列化、时间戳转为秒级整数、空字符串映射为 null 等场景,都得靠自定义转换器控制。
注意:泛型参数 T 必须是具体类型(如 JsonConverter<datetimeoffset></datetimeoffset>),不能是基类或接口(如 JsonConverter<object></object>)——否则运行时会抛 NotSupportedException。
-
Read方法里用ref Utf8JsonReader reader逐字段解析,别直接调JsonSerializer.Deserialize<t>(reader)</t>,否则会递归触发自身转换器 -
Write方法中用Utf8JsonWriter手动写入字段,避免调JsonSerializer.Serialize(writer, value)引发循环 - 若需复用默认行为(例如只改某个字段),可在
Read中先用JsonSerializer.Deserialize<t>(ref reader, options)</t>得到中间对象,再后处理
JsonConverter 怎么注册到 JsonSerializerOptions
注册方式决定作用域:全局生效还是局部覆盖。最常用的是往 JsonSerializerOptions.Converters 集合里加实例,但顺序很重要——靠前的转换器优先匹配类型。
- 添加
new DateTimeOffsetConverter()会覆盖默认的DateTimeOffset处理逻辑 - 若同时注册了
JsonConverter<object></object>(不推荐),它会捕获所有未明确指定的类型,导致其他转换器失效 - 局部使用时,可给属性加
[JsonConverter(typeof(MyConverter))]特性,优先级高于全局注册项 - 使用源生成器(
JsonSerializerContext)时,转换器必须通过JsonSerializerOptions传入,特性注册无效
常见踩坑:DateTime / DateTimeOffset 转换器为什么没生效
不是写了转换器就自动生效。System.Text.Json 对日期类型有硬编码的内置处理逻辑,会绕过用户注册的转换器,除非你显式禁用默认行为。
- 必须设置
options.DateTimeZoneHandling = DateTimeZoneHandling.Utc或类似选项(实际是JsonSerializerOptions的DefaultIgnoreCondition不影响此行为) - 更可靠的做法:在转换器里检查
reader.TokenType == JsonTokenType.String,再手动解析 ISO 格式;避免依赖reader.GetDateTimeOffset(),它可能已被内部逻辑拦截 - 如果目标是把
"2023-01-01"解析为DateTime而非报错,转换器里要用reader.GetString()拿原始字符串,再用DateTime.TryParseExact处理
性能敏感场景下 JsonConverter 的写法要点
高频序列化时,JsonConverter 是性能瓶颈点之一。避免反射、避免临时字符串分配、尽量复用 Utf8JsonWriter 内部缓冲区。
- 不要在
Write中拼接 JSON 字符串再写入(如writer.WriteStringValue(JsonSerializer.Serialize(value))),这会触发二次序列化和 UTF-8 编码 - 读取字符串字段时,优先用
reader.GetString(),而非reader.ValueSpan.ToString()—— 后者会创建新字符串对象 - 若转换逻辑简单(如布尔值映射为 "Y"/"N"),直接用
switch (reader.GetString()),比反序列化成中间对象快 3–5 倍 - 调试时开启
options.WriteIndented = true会显著拖慢速度,上线前务必关闭
JsonConverter 最容易被忽略的,是它和默认序列化路径的耦合关系——你以为自己接管了全部逻辑,其实某些类型(如 Dictionary<string object></string> 或含 object 成员的类)仍会走默认分支,导致行为不一致。动手前先用最小示例验证边界输入。










