Serde 元编程

第 14 章 Enum 的四种 tag 策略

作者 杨艺韬 · 8,500 字 · 发布于 · 更新于

14.1 同一个 enum,四种 JSON 编码

Serde 对 enum 的序列化支持四种互不兼容的策略。同一个 enum:

#[derive(Serialize, Deserialize)]
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

能编码成四种完全不同的 JSON:

// 1. Externally tagged(默认)
{"Circle": {"radius": 3.0}}

// 2. Internally tagged:#[serde(tag = "type")]
{"type": "Circle", "radius": 3.0}

// 3. Adjacently tagged:#[serde(tag = "t", content = "c")]
{"t": "Circle", "c": {"radius": 3.0}}

// 4. Untagged:#[serde(untagged)]
{"radius": 3.0}

每种格式都有自己的用途:

  • Externally:Rust 社区默认,最"朴实"。
  • Internally:匹配许多 REST API 的惯例({"type": "user", "name": "alice"})。
  • Adjacently:tag 与内容分成两个字段,结构最规整,常用于跨语言、跨系统的互操作。
  • Untagged:反序列化时"猜"是哪个变体——用于宽容地接受多种形态输入(如 TOML 配置文件里的 "单个字符串 or 字符串数组")。

四种策略,四种完全不同的序列化/反序列化逻辑。serde_derive 的 de/ 目录里有四个独立文件处理这件事。本章把这四种模式一网打尽——它们的设计意图、生成代码、边缘情况、使用场景。

这是第 10-14 章系列的收尾,读完你就理解了 serde_derive 的全部 enum 处理。

14.2 为什么 enum 有这么多 tag 模式

本质原因是"识别是哪个变体"这个问题有不同解法。一个 enum 有多个变体,反序列化时必须知道输入对应哪个变体。"tag"就是这个识别信息——告诉 deserializer "这是 Circle 不是 Rectangle"。

Tag 可以放在哪里?

  • 外面(externally):在变体的"外壳"上。形式是 {"VariantName": value}——整个 JSON 对象的 key 就是变体名。
  • 里面(internally):和变体字段混在一起。变体是 struct 时,加一个专门字段表示 "type"。
  • 相邻(adjacently):分两个字段,一个放 tag,一个放内容。
  • 没有(untagged):根本不写 tag。反序列化时试着匹配每个变体,第一个成功的胜出。

每种设计都有权衡:

策略 优点 缺点
External 默认、无歧义、解析最快 多一层嵌套 {"V": ...}
Internal 扁平结构(匹配典型 REST API) 不能用于 tuple variant(没有字段名,tag 无处可放)
Adjacent 清晰分离、可处理所有变体类型 字段名多两个(tag 和 content)
Untagged 最灵活、适合"多种形态接受" 反序列化慢(要尝试)、有歧义风险

本章每一节对应一种模式。先从默认的 externally 开始。

14.3 Externally tagged(默认)

这是 Rust 社区的默认选择——也是不写任何 tag 属性时的模式(四种模式在 v1.0.0 发布时就已齐备:v1.0.0 的 serde_derive/src/de.rs 里已经有 internally / adjacently / untagged 三条生成路径)。

JSON 编码:

{"Circle": {"radius": 3.0}}
{"Rectangle": {"width": 1.0, "height": 2.0}}

关键特征:JSON 对象恰好 1 个 key——这个 key 就是变体名。value 是变体的内容。

序列化生成代码(serde_derive/src/ser.rs:502,serialize_externally_tagged_variant):

对 Circle { radius: 3.0 },生成代码调用:

_serde::Serializer::serialize_struct_variant(
    __serializer,
    "Shape",          // enum name
    0u32,             // variant index
    "Circle",         // variant name
    0 + 1,            // field count(#len 展开后的原样)
)
// 然后 serialize_field("radius", &radius), .end()

serialize_struct_variant 是 Serializer trait 的专用方法(第 3 章讲过)——它接收所有关键信息(enum名/索引/变体名/字段数),让格式自己决定编码。JSON 格式把它编码成 {"Circle": {"radius": 3.0}}。

反序列化比序列化复杂。看 serde_derive/src/de/enum_externally.rs(全文 213 行):

生成代码大致是下面这个形状——示意,非 serde_derive 源码原文:宏源码是 quote! 模板,__Field / __FieldVisitor / visit_str 这些名字要展开之后才成形。

其中 visit_enum 那一段的模板在 serde_derive/src/de/enum_externally.rs,变体标识符 visitor(visit_str 那段)的模板则在 serde_derive/src/de/identifier.rs。

impl<'de> Deserialize<'de> for Shape {
    fn deserialize<__D>(d: __D) -> Result<Self, __D::Error> where __D: Deserializer<'de> {
        // 1. 定义"变体标识符" enum
        enum __Field { __field0, __field1 }  // 变体按序号命名
        struct __FieldVisitor;
        impl<'de> Visitor<'de> for __FieldVisitor {
            type Value = __Field;
            fn visit_str<E: Error>(self, v: &str) -> Result<__Field, E> {
                match v {
                    "Circle" => Ok(__Field::__field0),
                    "Rectangle" => Ok(__Field::__field1),
                    _ => Err(Error::unknown_variant(v, VARIANTS)),
                }
            }
            // visit_u64 for index-based encoding, visit_bytes, ...
        }

        // 2. 主 Visitor
        struct __Visitor;
        impl<'de> Visitor<'de> for __Visitor {
            type Value = Shape;
            fn expecting(&self, f: &mut Formatter) -> fmt::Result {
                f.write_str("enum Shape")
            }

            // externally tagged 走 visit_enum
            fn visit_enum<__A>(self, __data: __A) -> Result<Shape, __A::Error>
            where __A: EnumAccess<'de>
            {
                // 读 tag(变体名)
                let (variant, __variant_data) = __data.variant::<__Field>()?;
                match variant {
                    __Field::__field0 => {
                        // 把 variant_data 当作 Circle 变体的字段数据
                        VariantAccess::struct_variant(__variant_data, &["radius"], CircleVisitor)
                    }
                    __Field::__field1 => {
                        VariantAccess::struct_variant(__variant_data, &["width", "height"], RectVisitor)
                    }
                }
            }
        }

        const VARIANTS: &[&str] = &["Circle", "Rectangle"];
        d.deserialize_enum("Shape", VARIANTS, __Visitor)
    }
}

关键步骤:

  1. 变体标识符:和 struct 的字段标识符 __Field 结构相同(见第 13 章)。
  2. 主 Visitor 实现 visit_enum:这是 externally tagged 独有的——Deserializer 会先调用 visit_enum。
  3. EnumAccess::variant 读 tag:第一步读出变体名(作为 __Field)。
  4. VariantAccess 读变体数据:第二步根据 variant 读对应的 struct/tuple/unit/newtype 数据。

EnumAccess 和 VariantAccess是第 4 章提到的两个状态机 trait:

// 精简:省略了两个 trait 的 `: Sized` 约束,以及带默认实现的 `variant` / `newtype_variant`
pub trait EnumAccess<'de> {
    type Error: Error;
    type Variant: VariantAccess<'de, Error = Self::Error>;

    fn variant_seed<V: DeserializeSeed<'de>>(self, seed: V)
        -> Result<(V::Value, Self::Variant), Self::Error>;
}

pub trait VariantAccess<'de> {
    type Error: Error;
    fn unit_variant(self) -> Result<(), Self::Error>;
    fn newtype_variant_seed<T: DeserializeSeed<'de>>(self, seed: T) -> Result<T::Value, Self::Error>;
    fn tuple_variant<V: Visitor<'de>>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>;
    fn struct_variant<V: Visitor<'de>>(self, fields: &'static [&'static str], visitor: V) -> Result<V::Value, Self::Error>;
}

这是 Serde 为 enum 反序列化专门设计的两阶段 API:EnumAccess 读 tag、VariantAccess 读内容。格式层面 JSON 看到 {"Circle": {...}} 后,Deserializer 把 "Circle" 作为 tag,{...} 作为 variant data,分别给用户的 Visitor。

14.4 Internally tagged:扁平化

Internally tagged 适合匹配 "REST API" 风格的 JSON:

#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum Event {
    Click { x: i32, y: i32 },
    KeyPress { key: char },
}

序列化成:

{"type": "Click", "x": 10, "y": 20}
{"type": "KeyPress", "key": "a"}

Tag 嵌在变体字段里——和变体的 x、y、key 一起组成一个扁平 JSON 对象。

序列化生成代码(serde_derive/src/ser.rs:575):

对 Click { x: 10, y: 20 },生成:

// 先调用 serialize_struct(字段数比变体字段数多 1,因为加了 tag 字段;ser.rs:943 的 `#len + 1`,#len 展开为 0 + 1 + 1)
let mut state = _serde::Serializer::serialize_struct(__serializer, "Event", 0 + 1 + 1 + 1)?;

// 先写 tag
SerializeStruct::serialize_field(&mut state, "type", "Click")?;

// 然后写变体字段
SerializeStruct::serialize_field(&mut state, "x", &x)?;
SerializeStruct::serialize_field(&mut state, "y", &y)?;

SerializeStruct::end(state)

注意是 serialize_struct 而不是 serialize_struct_variant——因为 tag 和字段混在一起,格式层面不能区分"这是一个 enum variant"。直接伪装成 struct 输出。

反序列化更巧妙(serde_derive/src/de/enum_internally.rs,全文 106 行)。生成代码大致是:

// enum_internally.rs:54-61 生成的形状(精简)
// 分两阶段:先用 TaggedContentVisitor 走一遍输入——拆出 tag、其余内容缓存成 Content;
// 再用 tag 决定走哪个变体
let (__tag, __content) = _serde::Deserializer::deserialize_any(
    __deserializer,
    _serde::__private228::de::TaggedContentVisitor::<__Field>::new("type", "internally tagged enum Event"),
)?;
let __deserializer = _serde::__private228::de::ContentDeserializer::<__D::Error>::new(__content);

match __tag {
    __Field::__field0 => {
        // 用缓存的 content 构造一个新的 Deserializer,按 struct 形式反序列化 Click 的字段
        /* struct_::deserialize(.., StructForm::InternallyTagged(Click)) 生成的 visitor */
    }
    __Field::__field1 => { ... }
}

关键是 Content 中间类型——TaggedContentVisitor::visit_map(serde/src/private/de.rs:859)遍历一遍 map:遇到 tag key 就读出 __Field,其余 key/value 都缓存进 Content::Map;然后:

  1. tag 找不到直接 missing_field;重复出现 duplicate_field
  2. 根据 tag 决定类型,用 ContentDeserializer 从缓存的 Content 再反序列化一次

为什么要缓存? 因为输入是流式的,tag 可能在最后({"x": 10, "y": 20, "type": "Click"})。反序列化 Click { x, y } 时需要从头开始读 x、y,但流已经走完。缓存让我们可以任意顺序重读。

缺点:内存占用。对大 JSON 对象,缓存可能显著增加内存。代价换来的是 tag 可以出现在对象的任意位置。

14.5 Adjacently tagged:清晰分离

Adjacently tagged 把 tag 和 content 放在明确的两个字段:

#[derive(Serialize, Deserialize)]
#[serde(tag = "t", content = "c")]
enum Command {
    Start,
    Stop,
    Move { dx: i32, dy: i32 },
}

序列化:

{"t": "Start"}
{"t": "Stop"}
{"t": "Move", "c": {"dx": 5, "dy": 3}}

Unit 变体 Start/Stop 没有 content 字段——因为没有内容。其他变体把所有字段放到 c 下。

和另外几种比:相对 Internal,它能用于全部四种变体形态(Internal 不支持 tuple variant,见 §14.10 坑 1),tag 也不会和变体字段撞名(Internal 要靠 check_internal_tag_field_name_conflict 在编译期拦截这种冲突,见第 11 章 §11.8.2);相对 External,tag 和内容都放在固定名字的字段里,变体名是字段的值而不是 key;相对 Untagged,它有明确的 tag,不必试错。代价是 content 先于 tag 出现时要缓冲(下文)。

序列化生成代码(serde_derive/src/ser.rs:641):

对 Move { dx: 5, dy: 3 }:

let mut state = _serde::Serializer::serialize_struct(__serializer, "Command", 2)?;
SerializeStruct::serialize_field(&mut state, "t",
    &_serde::__private228::ser::AdjacentlyTaggedEnumVariant {
        enum_name: "Command", variant_index: 2u32, variant_name: "Move",
    })?;
SerializeStruct::serialize_field(&mut state, "c",
    &__AdjacentlyTagged { data: (dx, dy), phantom: PhantomData::<Command> })?;
SerializeStruct::end(state)

注意 &__AdjacentlyTagged { data: (dx, dy), .. }——这是生成代码里定义的临时 struct(类似第 12 章的 SerializeWith),字段是一个引用元组。它包装变体字段并实现 Serialize,使它们能被当作一个整体序列化为 "c" 字段的 value。tag 也不是裸字符串——AdjacentlyTaggedEnumVariant 把它作为 unit variant 序列化,JSON 里才呈现为 "Move"。

对 Unit 变体 Start:

let mut state = _serde::Serializer::serialize_struct(__serializer, "Command", 1)?;
SerializeStruct::serialize_field(&mut state, "t", &AdjacentlyTaggedEnumVariant { /* Start */ })?;
SerializeStruct::end(state)

只写 tag,没有 content——匹配 JSON {"t": "Start"}。

反序列化(serde_derive/src/de/enum_adjacently.rs,全文 324 行):生成代码很长。核心逻辑:

  1. 外层是 struct Visitor,只关心两个 key:t 和 c(默认其余 key 用 IgnoredAny 跳过;deny_unknown_fields 时换成遇到未知 key 就报错的 TagOrContentFieldVisitor,serde_derive/src/de/enum_adjacently.rs:56-60)。
  2. 读 t 时:解析出变体名,存起来。
  3. 读 c 时:必须等 t 已经读到,否则先把 c 的内容暂存为 Content(中间类型),然后等 t 出现后再解析。
  4. t 出现后:根据变体名决定 c 的类型,用对应 Visitor 解析 c。

处理 tag 和 content 顺序不定是这里的关键难点。和 internally tagged 类似用 Content 缓存。

边缘情况:

  • t 出现但没有对应的 c:如果变体是 unit,OK;newtype 变体走 missing_field(content)(内部类型是 Option 时得到 None);其他变体报 missing_field("c")(serde_derive/src/de/enum_adjacently.rs:62-101)。
  • c 出现但没有 t:报 missing_field("t")。
  • t 或 c 重复出现:duplicate_field。
  • 其他字段出现:默认跳过;deny_unknown_fields 时报错。
  • 输入是序列 [tag, content]:也接受——visitor 同时实现了 visit_seq。

这 324 行代码就在处理这些情况。

14.5.1 TagOrContentField —— Adjacent 复杂度的核心

打开 serde_derive/src/de/enum_adjacently.rs:125-146——那里是一段 quote! 模板,带着 _serde::#private:: 前缀和一个 while let 循环;它生成出来的逻辑,去掉噪音后是这个形状:

// 下面这段不是源码原文:把模板里的 `_serde::#private::` 前缀和 `#next_key`
// 代入具体值、并简化路径之后,等价于这个形状(中文注释与缩写均为本专栏所加)
let mut __rk: Option<TagOrContentField> = None;
while let Some(__k) = __map.next_key_seed(TagContentOtherFieldVisitor { tag: "t", content: "c" })? {
    match __k {
        TagContentOtherField::Other => { __map.next_value::<IgnoredAny>()?; continue; }
        TagContentOtherField::Tag => { __rk = Some(TagOrContentField::Tag); break; }
        TagContentOtherField::Content => { __rk = Some(TagOrContentField::Content); break; }
    }
}

TagOrContentField 是 serde::__private228::de 里定义的内部 enum(serde/src/private/de.rs:892)——只有两个 variant:Tag / Content。字符串比对交给 TagContentOtherFieldVisitor / TagOrContentFieldVisitor 这两个 seed 做;生成代码只在归类后的两个 slot 上 match——避免每个 variant deserializer 重复写字符串比对。

源码 :151-160 和 :226-276 之所以重复出现 TagOrContentField match——是因为 Adjacent 反序列化要在两套状态机里工作:

  • 状态 A——还没读到 tag、当前在解析"接下来这个 key 是 t 还是 c"
  • 状态 B——已经读到 tag、当前在期待 c 出现

两种状态各自需要 match TagOrContentField——所以这个 enum 名在 324 行源码里出现了 11 次(grep -ow TagOrContentField 计数)。这种"同一原语在多状态下复用"的写法,是 derive 生成代码"重复但机械"的根本原因:#next_relevant_key 这段 while let 模板在源码里只写一次,却被插值进多个状态分支,展开后的代码里会出现好几份(上面的两变体 Shape 实测展开出 5 份)。

14.6 Untagged:反序列化的"猜猜看"

最灵活也最慢的模式:

#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum JsonValue {
    Number(f64),
    String(String),
    Array(Vec<JsonValue>),
}

序列化:

42.0           // Number(42.0)
"hello"        // String("hello")
[1.0, 2.0, 3.0]  // Array([Number(1.0), Number(2.0), Number(3.0)])

完全没有 tag——靠反序列化时尝试每个变体,哪个成功就用哪个。

序列化生成代码(serde_derive/src/ser.rs:765):对 Number(42.0):

// 宏展开后的等价代码,不是源码原文
_serde::Serialize::serialize(&42.0_f64, __serializer)

就是字段的 serialize——完全忽略 enum 存在。格式层面看到的就是一个 f64。

反序列化(serde_derive/src/de/enum_untagged.rs,全文 135 行)最精妙:

// enum_untagged.rs:46-57 生成的形状(精简)
let __content = DeserializeSeed::deserialize(ContentVisitor::new(), __deserializer)?;  // 先缓存
let __deserializer = ContentRefDeserializer::<__D::Error>::new(&__content);  // 只借用,可重复使用

// 按顺序尝试每个变体
if let Ok(__ok) = Result::map(<f64 as Deserialize>::deserialize(__deserializer), JsonValue::Number) {
    return Ok(__ok);
}
if let Ok(__ok) = Result::map(<String as Deserialize>::deserialize(__deserializer), JsonValue::String) {
    return Ok(__ok);
}
if let Ok(__ok) = Result::map(<Vec<JsonValue> as Deserialize>::deserialize(__deserializer), JsonValue::Array) {
    return Ok(__ok);
}

Err(Error::custom("data did not match any variant of untagged enum JsonValue"))

思路:把输入先缓存为 Content,然后逐个尝试反序列化为每个变体的字段类型。第一个成功的返回。

14.6.1 Content<'de> 真身:22 变体的万能缓冲,以及它的退役计划

前面代码里反复出现的 Content 是什么?打开 serde_core/src/private/content.rs:10:

// Used from generated code to buffer the contents of the Deserializer when
// deserializing untagged enums and internally tagged enums.
//
// Not public API. Use serde-value instead.
//
// Obsoleted by format-specific buffer types (https://github.com/serde-rs/serde/pull/2912).
#[doc(hidden)]
pub enum Content<'de> {
    Bool(bool),
    U8(u8), U16(u16), U32(u32), U64(u64),
    I8(i8), I16(i16), I32(i32), I64(i64),
    F32(f32), F64(f64),
    Char(char),
    String(String), Str(&'de str),           // ← zero-copy 双形
    ByteBuf(Vec<u8>), Bytes(&'de [u8]),       // ← zero-copy 双形
    None,
    Some(Box<Content<'de>>),
    Unit,
    Newtype(Box<Content<'de>>),
    Seq(Vec<Content<'de>>),
    Map(Vec<(Content<'de>, Content<'de>)>),
}

实测 22 个变体(按源码:1 个 Bool + 4 个 Uxx + 4 个 Ixx + 2 个 Fxx + Char + String + Str + ByteBuf + Bytes + None + Some + Unit + Newtype + Seq + Map = 22)——几乎把 Serde Data Model 的每个原语都缓冲一份。几个值得展开的点:

1. Str(&'de str) 和 String(String) 并存、Bytes(&'de [u8]) 和 ByteBuf(Vec<u8>) 并存——前者是零拷贝、后者是拥有所有权。反序列化时如果原始输入有足够长的生命周期('de)、就走零拷贝的 Str/Bytes 变体;否则走 String/ByteBuf。同一份 Content 同时能表示借用和拥有——untagged enum 反序列化时想保留 zero-copy 可能性,又要能处理流式输入、只能两套都支持。

2. 源码注释有三条元信息:

  • "Used from generated code"——这个 enum 只被 #[derive(Deserialize)] 展开的代码访问、不是给用户用的
  • "Not public API. Use serde-value instead."——如果你想要"运行时可检查的 Serde 值"、官方推荐用 serde-value crate、不要直接用 Content
  • "Obsoleted by format-specific buffer types (PR #2912)"——指向一个替代方向:PR #2912(dtolnay 的"Customizable buffer type (proof of concept)",截至本专栏写作时仍是 draft,未合并)演示了一种向后兼容的做法,让 serde_json 这样的格式用自己的值类型(如 serde_json::Value)代替通用的 Content 给 untagged enum 和 flatten 做缓冲,要解决的是 issue #1183"Internal buffering disrupts format-specific deserialization features"——经过通用缓冲之后,格式特有的反序列化能力会丢失(例如 JSON 里字符串形式的 map key 无法再解析成 u32)。

3. #[doc(hidden)] + pub 的组合是 Rust 生态里"公开导出但不承诺稳定"的标准手法。类型技术上可访问、文档里不显示、语义上可以任意改动——用户误用 Content 不会有任何编译期提示,只能靠模块名 __private228 自觉。

14.6.2 ContentRefDeserializer 的重放机制

untagged enum 的关键技术——一次缓冲、多次尝试——靠 ContentRefDeserializer(serde/src/private/de.rs:1992)实现:

pub struct ContentRefDeserializer<'a, 'de: 'a, E> {
    content: &'a Content<'de>,   // ← 借用 Content,不拷贝
    err: PhantomData<E>,
}

关键是它只借用 &Content、不消费——每次 deserialize() 调用都从 content 重新开始"读"一遍、模拟一个全新的反序列化过程。'de: 'a lifetime bound 保证 Content 里的 &'de str 借用能穿透 ContentRefDeserializer 传递到 Visitor。

这让 untagged 的代码写成:

let __content = DeserializeSeed::deserialize(ContentVisitor::new(), __deserializer)?;  // 消费原始 deserializer 一次、缓冲
let __deserializer = ContentRefDeserializer::<__D::Error>::new(&__content);  // Copy 类型,可反复传值
if let Ok(v) = Variant1::deserialize(__deserializer) {
    return Ok(v);  // 成功
}
if let Ok(v) = Variant2::deserialize(__deserializer) {
    return Ok(v);  // 同一个 content 被重放
}
// ...

这种"输入 → 内存对象 → 重放 N 次" 的两阶段架构是 untagged 慢的根本原因:每次尝试都要遍历 content 树一次、不是简单地前向解析一次输入。PR #2912 试图替换的正是这种"在 Serde 通用层做缓冲"的架构——让格式用自己的值类型(如 serde_json::Value)做缓冲,从而保留格式特有的语义。

两个问题:

1. 性能:每次反序列化都可能尝试多个变体,每个尝试要遍历 Content 一次。对深度嵌套的 untagged enum,性能会比 tagged 模式明显慢——具体倍数取决于变体数量和输入深度,读者应在自己场景实测(第 15 章会用到的 criterion 方法可直接套用)。

2. 歧义:如果两个变体能匹配同样的输入会怎么样?比如 A { x: i32 } 和 B { x: i32, y: i32 }:输入 {"x": 1, "y": 2} 两个都能接受(A 默认忽略多余的 y)。按变体声明顺序,前者优先。这意味着变体顺序影响反序列化结果——容易踩坑。

Untagged 的典型使用场景:

  • 配置文件的"宽容接受":接受 mode: "fast" 或 mode: {"level": 3} 等多种形态。
  • API 版本兼容:旧 API 返回 {"id": 1},新 API 返回 {"id": 1, "version": 2},untagged 可以同时处理。
  • JSON 的 "any value" 类型:serde_json::Value 表达的就是这种"任何形态都接受"的语义——不过它的 Deserialize 是手写的(serde_json 的 src/value/de.rs:151 直接调 deserialize_any(ValueVisitor)),不经过 untagged 试错。

14.6.3 de/ 目录的真实文件大小:5 个 enum 文件 + 4 个通用文件

serde_derive/src/de/ 实测包含 9 个文件、共 2383 行——5 个 enum_*.rs 加 identifier.rs / struct_.rs / tuple.rs / unit.rs 四个通用文件——本章前面散见的"X 行"数据集中列出:

文件 行 角色
enum_.rs 96 4 种 tag 模式的分派入口(§14.7)
enum_externally.rs 213 External 反序列化生成
enum_internally.rs 106 Internal 反序列化生成
enum_adjacently.rs 324 Adjacent 反序列化生成(最复杂)
enum_untagged.rs 135 Untagged 反序列化生成
identifier.rs 477 __Field 标识符 enum 的生成器(4 种模式都用)
struct_.rs 697 struct 反序列化(不限 enum)
tuple.rs 283 tuple struct/variant 处理
unit.rs 52 unit struct/variant 处理

两个反直觉的观察——

1. enum_untagged.rs 只有 135 行——比 enum_adjacently.rs 短一倍多。直觉上 untagged 应该最复杂(要"猜"是哪个 variant)——但实际复杂性外包给了 Content 缓冲 + ContentRefDeserializer 重放机制(§14.6.1-14.6.2),生成代码本身只是"按变体顺序逐个 try"——非常机械。

2. enum_adjacently.rs 324 行,在 4 个 tag 文件里最大——超过另外 3 种 tag 文件的七成(213+106+135=454、单个 324 占其中 71%)。原因是 §14.5 末尾讲过的"tag 和 content 顺序不定"——必须支持 {"t":...,"c":...} 和 {"c":...,"t":...} 两种 JSON 顺序、且 t 先到 vs c 先到时处理路径完全不同(c 先到必须缓冲到 Content 等 t 出现)。

14.6.4 identifier.rs 477 行——所有 tag 模式的共同基础设施

被本章前文反复使用的 __Field enum 生成器在 serde_derive/src/de/identifier.rs——所有 4 种 tag 模式都依赖它。

它做什么?—— 给定一个 enum 的变体名列表(["Circle", "Rectangle"]),生成一段代码:

enum __Field { __field0, __field1 }

impl<'de> Deserialize<'de> for __Field {
    fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
    where __D: Deserializer<'de> {
        struct __FieldVisitor;
        impl<'de> Visitor<'de> for __FieldVisitor {
            type Value = __Field;
            fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> { /* 索引模式 */ }
            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> { /* 名字模式 */ }
            fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E> { /* 字节模式 */ }
        }
        __deserializer.deserialize_identifier(__FieldVisitor)
    }
}

477 行支持的能力——

  • 三种 visit 路径:visit_u64(按索引)、visit_str(按名字)、visit_bytes(按字节字符串)——格式自由选用
  • __ignore variant:只出现在 struct 的字段标识符里(同一个生成器也负责字段名,__ignore 由 serde_derive/src/de/struct_.rs:685 传入),处理"不认识的字段"——deny_unknown_fields=false 时落到这里、跳过;true 时报错。变体标识符没有 __ignore,未知变体直接报 unknown_variant(serde_derive/src/de/identifier.rs:261-265),除非有 #[serde(other)]
  • alias 支持:#[serde(alias = "x")] 让 __FieldVisitor::visit_str 同时认 "x" 和原名
  • rename_all 应用:在生成 visit_str 的 match 表里、按 RenameRule 转换后的字符串作为 case(转换本身在第 11 章的属性系统里完成,identifier.rs 只消费结果)
  • #[serde(other)] 兜底变体:prepare_enum_variant_enum(serde_derive/src/de/enum_.rs:62-68)把标了 other 的变体作为 fallthrough

为什么这个文件 477 行也算合理——因为它不是 4 种 tag 模式的孤立工具、是所有"按 tag 选 variant"逻辑的共同前置步骤。tag 拿到后必须先转成 __Field 才能 dispatch、所以 identifier 的健壮性直接决定 4 种模式的健壮性。

和章节其他节的连接——§14.3 externally tagged 的代码示例里 let (variant, __variant_data) = __data.variant::<__Field>()?; 那个 __Field 就是 identifier.rs 生成的;§14.4 internally tagged 提取 tag 时也用同一套机制。identifier.rs 是 4 种 tag 模式背后的共同骨架——本章前面没明说、这里补上。

14.7 enum_.rs:分发入口

serde_derive/src/de/enum_.rs(96 行)是 enum 反序列化的分派入口。它根据 cont.attrs.tag() 决定调哪个具体模块:

// serde_derive/src/de/enum_.rs:15-53 (精简)
pub(super) fn deserialize(params: &Parameters, variants: &[Variant], cattrs: &attr::Container) -> Fragment {
    // 变体级 #[serde(untagged)]:前面的 tagged 变体先试一轮,失败再按 untagged 逐个试
    match variants.iter().position(|var| var.attrs.untagged()) {
        Some(variant_idx) => {
            let (tagged, untagged) = variants.split_at(variant_idx);
            let tagged_frag = Expr(deserialize_homogeneous_enum(params, tagged, cattrs));
            let first_attempt = quote! { /* if let Ok(__ok) = (|| #tagged_frag)() { return Ok(__ok); } */ };
            enum_untagged::deserialize(params, untagged, cattrs, Some(first_attempt))
        }
        None => deserialize_homogeneous_enum(params, variants, cattrs),
    }
}

fn deserialize_homogeneous_enum(params: &Parameters, variants: &[Variant], cattrs: &attr::Container) -> Fragment {
    match cattrs.tag() {
        attr::TagType::External => enum_externally::deserialize(params, variants, cattrs),
        attr::TagType::Internal { tag } => enum_internally::deserialize(params, variants, cattrs, tag),
        attr::TagType::Adjacent { tag, content } => enum_adjacently::deserialize(params, variants, cattrs, tag, content),
        attr::TagType::None => enum_untagged::deserialize(params, variants, cattrs, None),
    }
}

这是经典的"策略模式"——TagType 是策略 enum,每种策略有独立实现。新增 tag 模式只需要加一个 enum 变体和一个模块。外面多包的一层 deserialize 处理变体级 #[serde(untagged)]——允许一个 enum 里前几个变体走容器 tag 模式、末尾几个变体 untagged 兜底(serde_derive/src/internals/ast.rs:166 已检查 untagged 变体必须排在最后)。

enum_.rs 剩下的部分只有一个通用工具 prepare_enum_variant_enum(serde_derive/src/de/enum_.rs:56):生成 VARIANTS 常量和所有变体共用的 __Field enum——external / internal / adjacent 三种模式都调用它(untagged 不需要变体标识符);#[serde(other)] 变体在这里变成 __FieldVisitor 的 fallthrough 分支。

14.8 serialize 端的分派

Serialize 侧的分派在 serde_derive/src/ser.rs:421 的 serialize_variant 里(第 12 章见过):

// serde_derive/src/ser.rs:474-494
let body = Match(match (cattrs.tag(), variant.attrs.untagged()) {
    (attr::TagType::External, false) => serialize_externally_tagged_variant(...),
    (attr::TagType::Internal { tag }, false) => serialize_internally_tagged_variant(..., tag),
    (attr::TagType::Adjacent { tag, content }, false) => serialize_adjacently_tagged_variant(..., tag, content),
    (attr::TagType::None, _) | (_, true) => serialize_untagged_variant(...),
});

和 Deserialize 侧对称——包括变体级 #[serde(untagged)]:只要变体自己标了 untagged,不管容器是什么 tag 模式都走 serialize_untagged_variant。实测 ser.rs 里 4 个函数的位置与篇幅——

函数 起始行 大致长度
serialize_externally_tagged_variant 502 ~73 行
serialize_internally_tagged_variant 575 ~66 行
serialize_adjacently_tagged_variant 641 ~124 行(最长)
serialize_untagged_variant 765 ~39 行(最短)
serialize_struct_variant_with_flatten(专用辅助) 968 ~88 行

Adjacently 序列化端依然是 4 种里最长——和反序列化端 (324 行 vs 其他 106-213 行) 形成对称——根因相同:要构造 {"t": "name", "c": {...}} 双字段对象、且 unit/newtype/tuple/struct 四种 variant 形态各有不同 content 结构。

Untagged 序列化端只有 39 行——因为它"完全忽略 enum 存在"(§14.6 提过)——只是 forward 字段值的 serialize、生成代码极简。Untagged 的复杂度全在反序列化端。

struct variant 的特殊辅助 serialize_struct_variant_with_flatten(line 968)——serialize_struct_variant 一看到有字段带 #[serde(flatten)](serde_derive/src/ser.rs:890-892)就整体改走这条路:struct variant 不能再用带 len 的 serialize_struct_variant / serialize_struct,只能退化成 serialize_map 并用 FlatMapSerializer 把 flatten 字段摊进去——88 行代码就为 §14.10 坑 4 这一个边角场景。这是 serde "覆盖所有合法组合"代价的体现——某些组合在生产中很少有人用到、但代码必须正确处理。

整个 enum tag 系统用 TagType 枚举统一表达——这是 Serde 把复杂功能放进类型系统的典范。用户写 #[serde(tag = "...")] 时,属性解析器把它变成 TagType::Internal { tag: "..." };代码生成器根据 TagType 分派。中间没有字符串比较,全是类型匹配。

14.9 选择哪种 tag 的指南

用什么场景?实战建议:

默认用 External:

  • 如果你只在 Rust 内部存序列化数据,External 是最直接的:它对应 Data Model 里原生的 enum 原语,Bincode、Postcard 这类二进制格式可以把 tag 直接编码成变体索引(visit_u64 路径)。
  • 它是 Rust 社区的惯例。

接收现有 API 数据用 Internal/Adjacent:

  • 如果你要解析别人的 API(比如 AWS、Stripe),先看他们的文档——他们可能用 internal tag({"type": "...", ...})或 adjacent tag({"kind": "...", "data": {...}})。

写人类可读配置用 Untagged:

  • TOML/YAML 配置文件里,用户期望简洁。enum Port { Number(u16), Name(String) } 用 Untagged 允许用户写 port = 8080 或 port = "http",而不是默认 External 下的 port = { Number = 8080 }。

API 版本兼容用 Untagged:

  • 当你需要同时支持"旧数据"和"新数据"两种形态,Untagged 是常见做法(如果只是新增字段,Option 或 #[serde(default)] 就够了)。

不要在关键路径上使用 Untagged:它慢;而且整份输入要先缓冲成 Content、再按变体逐个尝试,输入很大或被刻意构造时,内存和 CPU 开销都会随之放大。

14.10 边缘情况和坑

坑 1:Internally tagged + Tuple variant 不支持。

#[serde(tag = "type")]
enum E {
    Tuple(i32, i32),  // ❌ 编译错!
}

因为 tuple variant 没有字段名,没地方放 tag。属性解析阶段就会拒绝这种组合(serde_derive/src/internals/attr.rs:637-645,报错 #[serde(tag = "...")] cannot be used with tuple variants)。

代码生成器里对应的分支因此直接写 unreachable!("checked in serde_derive_internals")(serde_derive/src/ser.rs:637、serde_derive/src/de/enum_internally.rs:104)。

坑 2:Untagged 的 Any 类型。

#[serde(untagged)]
enum V {
    Any(serde_json::Value),  // 匹配任何 JSON
    Specific(f64),             // 仅匹配数字
}

Any 永远先匹配成功,Specific 永远不会被用到。变体顺序至关重要——把"更具体"的放前面。

坑 3:Adjacently 的 unit variant。

#[serde(tag = "t", content = "c")]
enum E { A, B { x: i32 } }

A 的 JSON 是 {"t": "A"}(没有 c),B 的 JSON 是 {"t": "B", "c": {"x": 1}}。但如果反过来 {"t": "A", "c": null} 呢?允许——unit variant 的 content 可以是 null/missing。但 {"t": "B"} 没有 c?报错——struct variant 必须有 content。

坑 4:Struct variant + #[serde(flatten)] 字段。

这个组合会让生成的序列化代码复杂到需要专门的 serialize_struct_variant_with_flatten 函数(serde_derive/src/ser.rs:968)。原因是 flatten 字段的 key 数量编译期不可知,不能再走带 len 的 serialize_struct_variant / serialize_struct,只能退化成 serialize_map(None);对 internally tagged 还要先把 tag 当第一个 map entry 写进去。

14.11 和本系列其他专栏的关联

tag 模式是协议设计的普适概念,不只 Rust 有:

  • 《MCP 协议设计与实现》专栏第 3 章"JSON-RPC 与消息格式"深入讨论过 JSON-RPC 的消息格式(那一章给的是 TypeScript / Python SDK 的定义)——请求信封里 method 做 tag、params 做 content,两者是同级的兄弟字段,正是 adjacently tagged 的形状;而 Request / Response / Notification 三者之间靠 id / result / error 的有无来区分,没有统一的 tag 字段,对应的是 untagged。Serde 的 adjacently tagged 模式直接支持前者。
  • 《LangChain 设计与实现》专栏第 15 章讲 bind_tools 时提到的 OpenAI 工具描述格式 {"type": "function", "function": {"name": ..., "parameters": ...}},是经典的 adjacently tagged 形状(type 做 tag,function 做 content)。
  • OpenAPI 的 discriminator 关键字:OpenAPI 3.x 的 discriminator(JSON Schema 本身没有这个关键字) 对应 Serde 的 internally tagged——同一个概念,两个生态各自实现。

14.12 四种 tag 模式在源码里不是四个配置项

从用户视角看,enum 表示只是几个属性:默认、tag、tag + content、untagged。从 serde_derive 源码看,它们是四套不同控制流。

tag 模式 Deserialize 源码锚点 核心动作 为什么不同
External serde_derive/src/de/enum_externally.rs:106-114 调 Deserializer::deserialize_enum,让格式提供 variant tag 在对象外层,格式可以直接读到 variant 名
Internal serde_derive/src/de/enum_internally.rs:54-57 用 TaggedContentVisitor 拆出 tag 和 content tag 混在对象字段里,要先缓存内容
Adjacent serde_derive/src/de/enum_adjacently.rs:169-176 先构造 seed,再用 ContentDeserializer 反序列化 content tag 和 content 两个字段顺序可变,要同时处理重复和缺失
Untagged serde_derive/src/de/enum_untagged.rs:46-57 先读成 Content,再按变体顺序逐个尝试 输入里没有 tag,只能试错回溯
flowchart TD
  A[enum Deserialize] --> B{TagType}
  B -->|External| C[deserialize_enum]
  B -->|Internal| D[TaggedContentVisitor]
  B -->|Adjacent| E[ContentDeserializer + seed]
  B -->|Untagged| F[ContentRefDeserializer 逐变体尝试]

External 最接近 Serde Data Model 的 enum 原语。serde_derive/src/de/enum_externally.rs:118-159 再按 variant style 分派:unit 调 unit_variant,newtype 走 newtype helper,tuple 交给 tuple::deserialize,struct 交给 struct_::deserialize。也就是说,External 不需要把整个 JSON 对象先存下来,variant 名一旦确定,就能进入对应 payload 的 visitor。

Internal 和 Adjacent 的复杂度来自"tag 不在 enum 原语原生位置"。Internal 的 TaggedContentVisitor 必须扫描对象字段找到 tag,同时保留剩余内容;Adjacent 还要处理 tag 字段和 content 字段谁先出现、是否重复、unit variant 是否允许没有 content。这就是为什么 §14.6.3 的表里 Adjacent 文件最大,不是因为它概念最难,而是因为它的输入状态最多。

Untagged 是最灵活也最昂贵的。serde_derive/src/de/enum_untagged.rs:46-47 先用 ContentVisitor 把输入完整读成中间内容,再用 ContentRefDeserializer 反复借用这份内容;serde_derive/src/de/enum_untagged.rs:51-57 对每个变体尝试一次,成功就返回,全部失败才给统一错误。这个策略保证不消费输入流多次,但代价是需要缓存一份内容,并且变体顺序会变成语义的一部分。

这四种实现解释了本章给出的实践建议:性能敏感默认 External,互操作 API 用 Internal/Adjacent,配置兼容才用 Untagged。它不是风格偏好,而是由源码里的控制流决定的。External 是单次分派;Internal/Adjacent 是缓存后分派;Untagged 是缓存后多次尝试。选择 tag 模式,本质上是在选择反序列化控制流。

Serialize 侧同样按 tag 模式分叉,只是控制流更短。serde_derive/src/ser.rs:474-494 按 TagType 调到 serialize_externally_tagged_variant、serialize_internally_tagged_variant、serialize_adjacently_tagged_variant 或 serialize_untagged_variant。External 的 serde_derive/src/ser.rs:502-574 会直接调用 serialize_unit_variant / serialize_newtype_variant 等 enum 原语;Internal 的 serde_derive/src/ser.rs:575-640 会把 tag 当成 struct 字段写进去,并且 tuple variant 已经在前置检查中被排除;Untagged 的 serde_derive/src/ser.rs:765-803 则退化成直接序列化 payload,unit variant 写成 unit,新类型直接序列化字段。

这说明 enum tag 的不对称性很强:序列化时,Rust enum 的 variant 已经确定,代码只要选一种输出形状;反序列化时,输入形状要反推出 variant,才需要缓存、试错和 ContentDeserializer。理解这个不对称,能解释为什么本章大部分复杂度都在 Deserialize 侧。

把 tag 模式写进公共 API 前,最好用几份坏输入做反序列化测试:未知 tag、缺 content、重复字段、多个变体都可匹配。

14.13 本章小结

Serde 对 enum 支持四种互不兼容的序列化策略:

策略 属性 JSON 形状 何时使用
External 默认(无) {"V": ...} Rust 内部、性能优先
Internal #[serde(tag = "t")] {"t": "V", ...} 匹配 REST API 风格
Adjacent #[serde(tag = "t", content = "c")] {"t": "V", "c": ...} 跨系统互操作
Untagged #[serde(untagged)] 变体字段裸露 宽容接受多形态

实现层面:

  • 四种策略对应 TagType 枚举的四个变体。
  • 属性解析把 #[serde(...)] 转换成 TagType。
  • Serialize 和 Deserialize 都按 TagType 分派到独立生成函数。
  • Deserialize 侧 Internal/Adjacent/Untagged 都依赖 Content 中间类型做缓存(因为 tag 位置不定或完全没有 tag;Adjacent 只在 content 先于 tag 出现时才缓冲)。

关键权衡:性能(External)vs 灵活性(Untagged)vs 互操作性(Internal/Adjacent)。

第 10-14 章至此完整拆解了 serde_derive——从架构总览、属性系统、Serialize/Deserialize 代码生成到 enum 的 4 种 tag 策略。你现在能够:

  • 读懂 serde_derive 任何一段源码,知道它在整个架构里的位置。
  • 理解任何 #[serde(...)] 属性对生成代码的精确影响。
  • 自己实现类似规模的 derive 宏。

下一章进入第五部分 高阶主题。第 15 章讲借用反序列化——&'de str 是怎么从 'de 输入借用的,Serde 的生命周期故事。这是 Serde 最令人困惑也最精妙的一块。

动手实验

  1. 四种 tag 模式对比:写一个 3 变体的 enum,分别用 4 种 tag 属性,cargo expand 观察生成代码差异。对比 Serialize 和 Deserialize 侧各自的差异。
  2. untagged 的歧义:写一个 untagged enum,两个变体都是 struct { x: i32 } 和 struct { x: i32, y: i32 }。测试输入 {"x": 1} 和 {"x": 1, "y": 2} 分别匹配哪个。调换变体顺序再测试。
  3. 读 enum_adjacently.rs:324 行的 Adjacent 反序列化代码,找到处理"tag 和 content 顺序不定"的核心逻辑。这是四种模式里最复杂的生成逻辑。
  4. 思考:如果你要设计第 5 种 tag 模式(比如"tag 在元数据里不在数据里"),你会加什么?为什么 Serde 没加?

延伸阅读