近日,.NET 社区中一项基于 System.Text.Json 的高级序列化/反序列化技巧引发广泛关注。开发者可以通过自定义特性(Attribute)动态指定 JSON 属性名称,从而在运行时根据类型信息灵活映射字段。这一模式解决了传统固定属性名难以适应多态数据或动态 Schema 场景的痛点,为现代 API 开发提供了全新思路。
痛点:静态绑定如何限制序列化灵活性?
在典型的 JSON 序列化过程中,C# 类中的属性名与 JSON 键名一一对应,即使使用 JsonPropertyNameAttribute 也只能在编译期指定固定名称。然而,实际业务往往需要根据上下文动态变化——例如,多语言系统希望根据语言代码输出不同键名(如 Name_zh-CN、Name_en-US),或者金融系统中数字字段需要根据精度单位自动调整后缀(如 Amount_USD、Amount_EUR)。传统方案要么依赖多次反射重写,要么被迫使用 Dictionary<string, object> 牺牲类型安全。
核心实现:特性驱动的动态名称解析
本次被广泛讨论的方案,核心在于利用自定义特性携带类型信息,再通过自定义 JsonConverter 或 TypeInfoResolver 在序列化过程中动态决定属性名。具体而言,开发者需定义一个泛型特性,例如 DynamicJsonPropertyAttribute<T>,其中 T 代表运行时的类型参数。序列化时,转换器读取该特性,提取 T 的具体类型(如 typeof(Language.ZhCN)),并将其转换为目标字符串(如 "Name_zh-CN"),从而替换原属性名。
代码示例片段如下:
[AttributeUsage(AttributeTargets.Property)]
public class DynamicJsonPropertyAttribute<T> : Attribute
{
public string GetPropertyName(string baseName) => $"{baseName}_{typeof(T).Name}";
}
public class Product
{
[DynamicJsonProperty<Language.ZhCN>]
public string Name { get; set; }
}
接着,自定义 JsonConverter 在写入时通过反射获取属性上的特性,并调用 GetPropertyName 生成最终键名。反序列化时,同样根据 JSON 中存在的动态键名反向匹配到目标属性。
技术亮点:不改框架,扩展无限
这一方法完全基于 System.Text.Json 已有的扩展点(如 JsonConverterFactory、JsonSerializerContext),无需修改库本身。由于采用了泛型特性,类型信息在编译期即可保留,避免了运行时全反射的性能损耗。同时,支持 JSON 输出格式的按需定制,便于与后端多租户、国际化和动态表单等架构深度整合。
适用场景与注意事项
该模式非常适合以下场景:
- 多语言字段:根据当前语言动态切换属性名后缀。
- 可配置 Schema:API 消费端要求不同名称但相同含义的字段。
- 版本兼容:旧版 API 使用 username,新版使用 userName,通过特性参数选择性输出。
但需注意,动态属性名会降低 JSON 的可读性和确定性,部分静态类型检查工具可能失效。此外,反序列化时需考虑前缀匹配策略,避免属性名冲突。
社区反响与未来展望
微软 .NET 团队官方博客曾表示,System.Text.Json 致力于提供“尽可能高性能且可扩展的 JSON 处理方案”。这一动态属性技巧正是其扩展能力的鲜活案例。目前,已有 NuGet 包(如 Betalgo.JsonSerializer)将此模式封装为即用型组件。随着 .NET 8 进一步强化源生成器(Source Generator)对自定义特性的支持,未来这类动态序列化有望获得原生工具链的官方认可。
对于追求灵活性与类型安全平衡的开发者而言,这次发现无疑是一份宝贵的“技术红利”。建议读者在尝试前充分评估业务复杂度,并善用单元测试验证动态映射逻辑的健壮性。