C# | JSON 序列化中的多态接口处理:基于 Attribute 实现精准 $type 控制

C# JSON 序列化中的多态接口处理:通过 Attribute 控制 $type 字段生成

文章目录

在 C# 开发中,使用 JSON 进行数据的序列化与反序列化是极其常见的场景。然而,当数据结构中包含 接口抽象 类时,传统的序列化机制通常会遇到瓶颈:反序列化引擎不知道应该将 JSON 目标绑定到哪个具体的实现类上

为了解决这一问题,通用的做法是在序列化生成的 JSON 数据中加入一个类型标识字段(例如 $type),用于记录对象的完整类名或类型别名。但如果全局开启这一机制,会导致所有普通对象(如基础 DTO、实体类等)都强制带有额外的 $type 标识,既增加了传输体积,也降低 J S O N JSON JSON 的可读性。

本文将详细讲解如何在 C# 的两大主流 JSON 框架(System.Text.JsonNewtonsoft.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.JsonNewtonsoft.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. 方案优势分析

  1. 类型安全性与安全性(Security) :相比于直接记录完整的 C# 程序集类型全名(Fully Qualified Class Name),使用自定义别名(如 "Dog")能防止不受信任的 JSON 触发任意类型实例化漏洞(RCE 隐患)。
  2. 零全局影响 :未声明 [JsonPolymorphic] 的普通类完全不受影响。

三、方案二:Newtonsoft.Json (Json.NET)

对于传统项目或还在使用 Newtonsoft.Json 的场景,全局开启 TypeNameHandling.AllTypeNameHandling.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"和"无法反序列化"之间做二选一的权衡:

  1. 如果项目基于 .NET 7+ ,优先采用 System.Text.Json[JsonPolymorphic][JsonDerivedType]。这种模式结构清晰,显式且安全。
  2. 如果项目依赖 Newtonsoft.Json ,采用 ItemTypeNameHandling = TypeNameHandling.Auto 配合全局默认配置,即可做到无缝且轻量级的精准控制。
相关推荐
消失的旧时光-19433 小时前
第六篇:Redis Hash——一个用户对象应该保存成 JSON,还是保存成 Hash?
redis·json·哈希算法
一位狮子座的程序员5 小时前
如何用RAG解决AI智能体的知识盲区?
开发语言·c#
Nemo_XP6 小时前
C# gridlookupedit选中内容重复还原操作
服务器·前端·c#
SEO_juper6 小时前
2026年Schema自动注入实战:用Python批量给1000个页面加上JSON-LD,AI引用率实测提升38%
人工智能·python·json·seo·独立站
猿长大人7 小时前
C# | 函数式编程入门
开发语言·c#·.net
Wang's Blog8 小时前
AI Agent白手起家37: LangChain 输出解析器实战:文本、JSON、XML 与 Pydantic
xml·langchain·json
灵析表格8 小时前
json_ObjectToKV 函数深度研究报告
json·excel·wps·灵析表格·excel公式盒子
unityのkiven9 小时前
C# 中的奇异递归模板模式:MonoSingleton<T> 的实现
java·开发语言·c#
雪隐9 小时前
WPF + MVVM 实战系列02-告别 INPC 手写时代,做个体面的现代 WPF 人
前端·后端·c#