文章目录▼CloseOpen
- 为什么要自定义JsonConverter?默认的不够用吗?
- 手把手教你写第一个自定义JsonConverter:枚举转描述
- 第一步:创建Converter类,继承JsonConverter
- 第二步:重写WriteJson——序列化时转成描述
- 第三步:重写ReadJson——反序列化时转成枚举
- 第四步:让Converter生效——全局或局部配置
- 全局配置(以ASP.NET Core为例)
- 局部配置(标记属性)
- 避坑!那些我踩过的"血的教训"
- 常见场景速查表:不用再瞎琢磨
- 最后:试试吧,你会爱上这种"掌控感"
- 自定义JsonConverter和默认Converter冲突时,哪个生效?
- 反序列化时,前端传旧的数字字符串(比如"0"),自定义Converter能识别吗?
- CanConvert方法必须重写吗?不写会有什么问题?
- 自定义日期格式的Converter,需要处理可空DateTime(DateTime?)吗?
- 如何全局配置多个自定义JsonConverter?
为什么要自定义JsonConverter?默认的不够用吗?
先说说默认转换器的”软肋”——它只能处理标准类型(比如string、int、DateTime)和简单对象(属性都是标准类型的类),但碰到个性化需求就歇菜了。比如:
- 枚举转描述:默认序列化枚举是数字,前端要显示”在售”这种文案,得额外写映射逻辑;
- 自定义日期格式:默认输出ISO 8601格式(带”T”),但很多业务场景要”yyyy-MM-dd HH:mm”;
- 复杂对象拆平/合并:比如User里嵌套Address,想把Address的City、Street直接作为User的字段输出,不用嵌套;
- 特殊数据类型:比如IP地址、自定义结构体,默认转换器会序列化成毫无意义的字符串。
我之前做的电商项目就踩过枚举的坑——商品状态用了枚举,前端拿到数字后,每次加新状态都要同步更新前端的映射表,沟通成本特别高。后来写了个EnumDescriptionConverter,直接把枚举转成Description属性里的文字,前端不用改一行代码,问题秒解决。
手把手教你写第一个自定义JsonConverter:枚举转描述
我拿最常见的”枚举转描述”举例子,带你从0到1实现。不用担心代码复杂,我把每个步骤的逻辑都讲清楚,你跟着复制粘贴都能跑通。
第一步:创建Converter类,继承JsonConverter
你得新建一个类,比如叫EnumDescriptionConverter
(T是你要处理的枚举类型),继承System.Text.Json.Serialization.JsonConverter
。这个类的核心是三个方法:CanConvert
(告诉框架这个Converter能处理什么类型)、WriteJson
(序列化时把对象转成Json)、ReadJson
(反序列化时把Json转成对象)。
先看CanConvert
——它的作用是”过滤类型”,只有当传入的类型是目标枚举(比如ProductStatus
)或者它的可空类型(ProductStatus?
)时,才返回true
。我第一次写的时候没处理可空类型,结果 nullable 枚举序列化时 Converter 根本不生效,调试了半小时才发现问题。代码长这样:
public class EnumDescriptionConverter JsonConverter where T Enum
{
public override bool CanConvert(Type typeToConvert)
{
// 处理可空类型:比如ProductStatus?
var underlyingType = Nullable.GetUnderlyingType(typeToConvert);
return typeToConvert == typeof(T) || underlyingType == typeof(T);
}
}
第二步:重写WriteJson——序列化时转成描述
WriteJson
是把枚举值转成Description文字的关键。比如ProductStatus.OnSale
的Description是”在售”,我们要做的就是:
MemberInfo
; DescriptionAttribute
; 代码很直观,但要注意空值处理——如果枚举没有加Description,就默认输出枚举名称(比如”OnSale”),避免返回null。具体实现:
public override void WriteJson(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
{
// 反射获取枚举的DescriptionAttribute
var memberInfo = typeof(T).GetMember(value.ToString()).FirstOrDefault();
var description = memberInfo?
.GetCustomAttribute()?
.Description;
// 没有Description就用枚举名称兜底
writer.WriteStringValue(description ?? value.ToString());
}
第三步:重写ReadJson——反序列化时转成枚举
前端传回来的是”在售”这样的字符串,我们要把它转成对应的枚举值ProductStatus.OnSale
。这一步需要遍历枚举的所有字段,找到Description匹配的那个值。代码逻辑:
public override T ReadJson(Utf8JsonReader reader, Type typeToConvert, T existingValue, bool hasExistingValue, JsonSerializerOptions options)
{
var stringValue = reader.GetString();
if (string.IsNullOrEmpty(stringValue))
{
// 处理空值:返回枚举默认值(比如0)
return default;
}
// 遍历枚举的所有公共静态字段(即枚举值)
foreach (var field in typeof(T).GetFields(BindingFlags.Public | BindingFlags.Static))
{
var attribute = field.GetCustomAttribute();
// 匹配Description或枚举名称(兼容旧数据)
if ((attribute != null && attribute.Description == stringValue) || field.Name == stringValue)
{
return (T)field.GetValue(null);
}
}
// 没找到匹配项,抛异常提示
throw new JsonException($"无法将"{stringValue}"转换为枚举类型{typeof(T).Name}");
}
第四步:让Converter生效——全局或局部配置
写好Converter后,得告诉框架”用这个Converter处理枚举”。有两种方式:
Startup.cs
或Program.cs
里加一行代码,所有枚举都用这个Converter(适合通用场景); [JsonConverter]
标记(适合特殊场景,比如某个枚举要保留数字)。 全局配置(以ASP.NET Core为例)
在ConfigureServices
里加AddJsonOptions
:
services.AddControllers()
.AddJsonOptions(options =>
{
// 注册全局枚举转换器
options.JsonSerializerOptions.Converters.Add(new EnumDescriptionConverter());
});
局部配置(标记属性)
如果某个属性不需要转描述,比如”订单类型”要保留数字,就用[JsonConverter]
覆盖全局配置:
public class Order
{
// 局部用默认转换器(转数字)
[JsonConverter(typeof(JsonStringEnumConverter))]
public OrderType Type { get; set; }
// 全局用EnumDescriptionConverter(转描述)
public ProductStatus Status { get; set; }
}
避坑!那些我踩过的”血的教训”
我写Converter的时候踩过不少雷,今天把这些坑提前告诉你,省得你走弯路:
ProductStatus?
,CanConvert
里一定要判断Nullable.GetUnderlyingType(typeToConvert)
,否则Converter不生效; WriteJson
加了writer.Close()
,结果导致后续序列化全报错——JsonWriter是框架管理的,不用我们手动关闭; [JsonConverter]
标记了其他Converter,局部的会覆盖全局的(比如某个枚举要转数字,就用JsonStringEnumConverter
); ReadJson
里要同时处理”数字字符串”和”描述字符串”——比如"0"
和”在售”都能转成ProductStatus.OnSale
。常见场景速查表:不用再瞎琢磨
为了帮你快速对应”需求-实现”,我整理了一个常见场景速查表,直接照做就行:
需求场景 | 关键实现要点 | 示例代码片段 |
---|---|---|
枚举转描述 | 反射获取DescriptionAttribute;处理可空类型 | value.GetType().GetMember(value.ToString()).First().GetCustomAttribute().Description |
自定义日期格式(如”yyyy-MM-dd HH:mm”) | 重写WriteJson时用ToString指定格式;ReadJson时解析自定义格式 | writer.WriteStringValue(value.ToString(“yyyy-MM-dd HH:mm”)) |
嵌套对象拆平(如User.Address.City→User.City) | 序列化时直接写顶级字段;反序列化时重组对象 | writer.WriteString(“City”, value.Address.City) |
最后:试试吧,你会爱上这种”掌控感”
我第一次写Converter的时候,花了一下午才调试通,但学会之后,解决了好多之前头疼的问题——比如那个电商项目的枚举问题,用了Converter后,前端再也没来找我问过”这个数字是什么状态”;再比如日期格式的需求,以前要在每个Dto里写[JsonIgnore]
再加个自定义字段,现在一个Converter全搞定。
其实自定义JsonConverter没你想的那么复杂,核心就是”告诉框架怎么转对象→Json,怎么转Json→对象”。你要是按上面的步骤写了第一个Converter,欢迎回来留言告诉我效果——比如”枚举转描述成功了!”或者”反序列化时抛异常了,帮我看看”。要是遇到问题,也可以把代码贴出来,我尽量帮你排查。
对了,微软 docs 里也有详细的Converter教程(链接:System.Text.Json 自定义转换器指南),里面有更多示例,想深入学可以去看看。
你还遇到过哪些Json序列化的奇葩场景?比如”想把List转成用逗号分隔的字符串”、”想把字典的Key转成小写”,也可以分享出来,咱们一起想办法用Converter解决!
其实关于CanConvert要不要重写,得看你继承的是哪种JsonConverter。要是用泛型版本的JsonConverter——比如你写的是处理ProductStatus枚举的EnumDescriptionConverter——那CanConvert真不用费劲重写。因为泛型已经把类型卡得死死的了,框架一看这个Converter是JsonConverter,立刻就明白它是专门处理ProductStatus类型的,根本不用你再额外解释“我能处理啥”。我第一次写枚举Converter的时候,就没重写CanConvert,结果跑起来一点问题没有,序列化的时候直接把枚举转成了描述文字,特省心。
但要是你继承的是非泛型的JsonConverter,那CanConvert可千万不能省。我之前帮做ERP系统的朋友改过程序,他写了个处理DateTime的自定义Converter,想把日期转成“yyyy-MM-dd HH:mm”的格式,结果继承的是非泛型版本,还忘了重写CanConvert。跑起来之后发现日期格式还是默认的带T的样子,前端一个劲问“这个T是啥意思”。后来我帮他看代码,才发现问题出在这——非泛型的Converter没有类型限制,框架根本不知道它能处理DateTime啊!最后加上CanConvert方法,返回typeToConvert == typeof(DateTime)或者typeToConvert == typeof(DateTime?),框架才终于把DateTime类型的字段交给这个Converter处理,日期格式一下子就对了。所以非泛型的情况,CanConvert是框架识别Converter的“身份证”,没它真不行。
自定义JsonConverter和默认Converter冲突时,哪个生效?
当局部配置(如属性上的[JsonConverter]
)与全局配置同时存在时,局部配置优先级更高。例如全局配置了枚举转描述的Converter,但某个属性用[JsonConverter(typeof(JsonStringEnumConverter))]
标记,该属性会优先使用JsonStringEnumConverter
(转数字或枚举名称),而非全局的描述Converter。
反序列化时,前端传旧的数字字符串(比如”0″),自定义Converter能识别吗?
可以。只要在ReadJson
方法中处理数字字符串的转换逻辑(如将”0″解析为对应枚举值),就能兼容旧数据。比如枚举转描述的Converter,可同时处理描述文字(如”在售”)和数字字符串(如”0″),避免前端修改代码的成本。
CanConvert方法必须重写吗?不写会有什么问题?
如果继承的是泛型版本JsonConverter
,CanConvert
可以不重写——泛型已限定处理类型。但如果继承非泛型的JsonConverter
,必须重写CanConvert
来指定适用类型,否则框架无法识别该Converter的处理范围,导致不生效。
自定义日期格式的Converter,需要处理可空DateTime(DateTime?)吗?
需要。若业务中存在可空日期字段,需在CanConvert
里判断可空类型(用Nullable.GetUnderlyingType(typeToConvert)
),确保Converter能处理DateTime?
。否则可空日期的序列化/反序列化不会触发自定义Converter。
如何全局配置多个自定义JsonConverter?
在JsonSerializerOptions
的Converters
集合中依次添加即可。例如同时配置枚举转描述和自定义日期格式的Converter,可写为options.JsonSerializerOptions.Converters.Add(new EnumDescriptionConverter());
options.JsonSerializerOptions.Converters.Add(new CustomDateTimeConverter());
。框架会按添加顺序匹配Converter,若多个Converter能处理同一类型,优先使用先添加的。