
C# JSON 序列化中的多态接口处理:通过 Attribute 控制 $type 字段生成
文章目录
- [C# JSON 序列化中的多态接口处理:通过 Attribute 控制 type 字段生成](# JSON 序列化中的多态接口处理:通过 Attribute 控制 type 字段生成)
-
- 一、问题背景与核心需求
- 二、方案一:System.Text.Json (.NET 7+)
-
- [1. 核心特性说明](#1. 核心特性说明)
- [2. 代码实现](#2. 代码实现)
- [3. 序列化与反序列化测试](#3. 序列化与反序列化测试)
- [4. 方案优势分析](#4. 方案优势分析)
- 三、方案二:Newtonsoft.Json (Json.NET)
-
- [1. 核心特性说明](#1. 核心特性说明)
- [2. 代码实现](#2. 代码实现)
- [3. 序列化与反序列化测试](#3. 序列化与反序列化测试)
- 四、两种方案的技术对比与选型指南
- 五、总结
在 C# 开发中,使用 JSON 进行数据的序列化与反序列化是极其常见的场景。然而,当数据结构中包含 接口 或 抽象 类时,传统的序列化机制通常会遇到瓶颈:反序列化引擎不知道应该将 JSON 目标绑定到哪个具体的实现类上。
为了解决这一问题,通用的做法是在序列化生成的 JSON 数据中加入一个类型标识字段(例如 $type),用于记录对象的完整类名或类型别名。但如果全局开启这一机制,会导致所有普通对象(如基础 DTO、实体类等)都强制带有额外的 $type 标识,既增加了传输体积,也降低 J S O N JSON JSON 的可读性。
本文将详细讲解如何在 C# 的两大主流 JSON 框架(System.Text.Json 与 Newtonsoft.Json )中,通过 Attribute 特性实现精准控制,仅对需要的接口与类型添加 $type 标识。
一、问题背景与核心需求
假设存在如下的数据结构,其中包含接口类型与普通类型:
csharp
public interface IAnimal
{
string Name { get; set; }
}
public class Dog : IAnimal
{
string Name { get; set; }
public string BarkSound { get; set; } = "Woof";
}
public class NormalPerson
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Container
{
public IAnimal Animal { get; set; } // 接口类型,需要记录具体实现
public NormalPerson Person { get; set; } // 普通类型,无需类型标识
}
我们的期望输出效果如下:
- 接口字段 :包含
$type字段,确保反序列化时能准确实例化为Dog。 - 普通字段 :保持干净无污染,不包含
$type。
json
{
"Animal": {
"$type": "Dog",
"Name": "Buddy",
"BarkSound": "Woof"
},
"Person": {
"Name": "Alice",
"Age": 30
}
}
接下来我们将分别介绍在 System.Text.Json 和 Newtonsoft.Json 中的具体实现方案。
二、方案一:System.Text.Json (.NET 7+)
在 .NET 7 及更高版本中,微软为原生库 System.Text.Json 引入了对多态序列化(Polymorphic Serialization)的原生支持。这一功能完全基于 Attribute 进行配置,默认不会侵入未标记的类型。
1. 核心特性说明
[JsonPolymorphic]:标记在接口或基类上,声明该类型支持多态序列化。可用于配置标识符的属性名称(如TypeDiscriminatorPropertyName = "$type")。[JsonDerivedType]:指定可能的具体派生类或实现类,并为其分配一个识别码(Discriminator)。
2. 代码实现
csharp
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
// 1. 在接口上配置多态特性,声明派生类映射
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]
[JsonDerivedType(typeof(Dog), typeDiscriminator: "Dog")]
[JsonDerivedType(typeof(Cat), typeDiscriminator: "Cat")]
public interface IAnimal
{
string Name { get; set; }
}
public class Dog : IAnimal
{
public string Name { get; set; }
public string BarkSound { get; set; } = "Woof";
}
public class Cat : IAnimal
{
public string Name { get; set; }
public bool LikesFish { get; set; } = true;
}
// 2. 普通类(无需添加任何 Attribute)
public class NormalPerson
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Container
{
public IAnimal Animal { get; set; }
public NormalPerson Person { get; set; }
}
3. 序列化与反序列化测试
csharp
var data = new Container
{
Animal = new Dog { Name = "Buddy" },
Person = new NormalPerson { Name = "Alice", Age = 30 }
};
var options = new JsonSerializerOptions { WriteIndented = true };
// 序列化
string json = JsonSerializer.Serialize(data, options);
Console.WriteLine("--- Serialized JSON ---");
Console.WriteLine(json);
// 反序列化
var deserialized = JsonSerializer.Deserialize<Container>(json, options);
Console.WriteLine("\n--- Deserialized Type Check ---");
Console.WriteLine($"Animal is Dog: {deserialized.Animal is Dog}");
4. 方案优势分析
- 类型安全性与安全性(Security) :相比于直接记录完整的 C# 程序集类型全名(Fully Qualified Class Name),使用自定义别名(如
"Dog")能防止不受信任的 JSON 触发任意类型实例化漏洞(RCE 隐患)。 - 零全局影响 :未声明
[JsonPolymorphic]的普通类完全不受影响。
三、方案二:Newtonsoft.Json (Json.NET)
对于传统项目或还在使用 Newtonsoft.Json 的场景,全局开启 TypeNameHandling.All 或 TypeNameHandling.Auto 会给所有属性打上 $type。要实现局部控制,关键策略在于:全局保持 TypeNameHandling.None,仅在需要多态的接口或类定义上叠加特性。
1. 核心特性说明
[JsonObject(ItemTypeNameHandling = TypeNameHandling.Auto)]:标记在接口或基类上。它告知 Newtonsoft.Json,在处理该接口类型的成员或集合元素时,自动根据实际运行时类型 与声明类型 的差异来决定是否写入$type。
2. 代码实现
csharp
using System;
using Newtonsoft.Json;
// 1. 在接口上指定其子项的 TypeNameHandling 为 Auto
[JsonObject(ItemTypeNameHandling = TypeNameHandling.Auto)]
public interface IAnimal
{
string Name { get; set; }
}
public class Dog : IAnimal
{
public string Name { get; set; }
public string Bark { get; set; } = "Woof";
}
// 2. 普通类(不加任何特定特性)
public class NormalPerson
{
public string Name { get; set; }
public int Age { get; set; }
}
public class Container
{
public IAnimal Animal { get; set; }
public NormalPerson Person { get; set; }
}
3. 序列化与反序列化测试
csharp
var data = new Container
{
Animal = new Dog { Name = "Buddy" },
Person = new NormalPerson { Name = "Alice", Age = 30 }
};
// 注意:全局 Settings 保持默认(TypeNameHandling.None)
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented
};
// 序列化
string json = JsonConvert.SerializeObject(data, settings);
Console.WriteLine("--- Serialized JSON ---");
Console.WriteLine(json);
// 反序列化
var deserialized = JsonConvert.DeserializeObject<Container>(json, settings);
Console.WriteLine("\n--- Deserialized Type Check ---");
Console.WriteLine($"Animal is Dog: {deserialized.Animal is Dog}");
生成的输出:
json
{
"Animal": {
"$type": "YourNamespace.Dog, YourAssembly",
"Name": "Buddy",
"Bark": "Woof"
},
"Person": {
"Name": "Alice",
"Age": 30
}
}
四、两种方案的技术对比与选型指南
| 维度 | System.Text.Json (.NET 7+) | Newtonsoft.Json |
|---|---|---|
| 性能 | 高(原生零内存分配优化) | 中等 |
| 控制粒度 | 依赖 [JsonDerivedType] 显式枚举派生类 |
自动根据继承关系推断 |
| 类型标识形式 | 支持自定义别名(如 "Dog")或完整类名 |
默认记录程序集限定类名 |
| 开放性扩展 | 新增派生类必须在接口上注册(支持强约束) | 新增派生类无需修改接口特性(灵活但注意安全) |
| 适用场景 | 推荐新项目及高并发、安全要求较高的 API 系统 | 存在大量复杂继承链或历史老旧代码库 |
五、总结
针对 C# JSON 序列化中接口多态的支持问题,我们不必在"全盘增加 $type"和"无法反序列化"之间做二选一的权衡:
- 如果项目基于 .NET 7+ ,优先采用 System.Text.Json 的
[JsonPolymorphic]与[JsonDerivedType]。这种模式结构清晰,显式且安全。 - 如果项目依赖 Newtonsoft.Json ,采用
ItemTypeNameHandling = TypeNameHandling.Auto配合全局默认配置,即可做到无缝且轻量级的精准控制。