Skip to content

第 16 章 #[serde(with)]、remote、flatten:三个"扩展口"的实现机理

16.1 Serde 的三个救命属性

Serde 的属性系统里有三个特别复杂的——withremoteflatten。它们不是普通的"重命名"、"跳过"这种简单行为——它们每个都是 Serde 为了应对现实世界的复杂性专门设计的扩展口

  • #[serde(with = "mod::path")]:让单个字段使用自定义的序列化/反序列化函数。
  • #[serde(remote = "path::ToType")]:为不属于你 crate 的类型生成 Serialize/Deserialize。
  • #[serde(flatten)]:把字段的 struct 内容"展平"到父级 struct。

这三个属性在现实项目里不可或缺

  • with 让你为第三方类型(chrono::DateTimeuuid::Uuid)定制序列化格式。
  • remote 让你给标准库类型(std::time::Duration)或其他库类型加 Serialize 支持——无需修改它们的源代码。
  • flatten 让你应对"嵌套 struct 但 JSON 扁平"的现实——比如 API 响应里公共字段(statuscode)和特定字段混在一起。

但它们的实现也比普通属性复杂一个量级。每一个都触发 serde_derive 里独特的代码生成路径、生成临时 struct、包装类型转换——是"模板引擎"逻辑上的巅峰。

本章拆解这三个属性的实现机理——不是"用户怎么用"(官方文档已经讲得清楚),而是"它们生成的代码长什么样、为什么这么设计"。

本书基于 serde 1.0.228。

16.2 #[serde(with = "module")] 的实现

最简单的例子:给 chrono::DateTime<Utc> 用自定义格式序列化。

用户代码:

rust
use chrono::{DateTime, Utc};
use serde::{Serialize, Deserialize};

mod iso_format {
    use chrono::{DateTime, Utc};
    use serde::{Serializer, Deserializer};

    pub fn serialize<S: Serializer>(t: &DateTime<Utc>, s: S) -> Result<S::Ok, S::Error> {
        s.serialize_str(&t.to_rfc3339())
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<DateTime<Utc>, D::Error> {
        let s = String::deserialize(d)?;
        DateTime::parse_from_rfc3339(&s)
            .map(|dt| dt.with_timezone(&Utc))
            .map_err(serde::de::Error::custom)
    }
}

#[derive(Serialize, Deserialize)]
struct Event {
    #[serde(with = "iso_format")]
    timestamp: DateTime<Utc>,
}

#[serde(with = "iso_format")] 告诉 serde_derive:"这个字段不用默认的 Serialize/Deserialize;改为调用 iso_format::serializeiso_format::deserialize"。

挑战:serde_derive 生成的主流程里,字段的序列化是 state.serialize_field("name", &self.field)——第二个参数必须是实现了 Serialize 的值。iso_format::serialize 是一个函数,不是类型。

Serde 的解法:生成一个临时 wrapper struct——让它持有字段引用,并为它实现 Serialize(内部调用用户函数)。

生成代码(第 12 章讲过,这里完整呈现):

rust
// 字段位置生成
SerializeStruct::serialize_field(
    &mut state,
    "timestamp",
    {
        // 定义一个临时 struct
        struct __SerializeWith<'__a> {
            values: (&'__a DateTime<Utc>,),
            phantom: PhantomData<Event>,
        }

        impl<'__a> Serialize for __SerializeWith<'__a> {
            fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
                iso_format::serialize(self.values.0, s)  // 调用用户函数
            }
        }

        &__SerializeWith {
            values: (&self.timestamp,),
            phantom: PhantomData,
        }
    },
)?;

临时 struct __SerializeWith

  • 只持有对字段的引用(&'__a DateTime<Utc>)——不 own 数据,零拷贝。
  • 实现 Serialize,内部转发到用户函数。
  • PhantomData<Event> 标记它的"来源类型",帮编译器推导生命周期。

使用时:把这个临时实例的引用传给 serialize_fieldserialize_field 调用 __SerializeWith::serialize,它又调用 iso_format::serialize(&self.timestamp, s)——函数被包装成了 trait 实现

这是 Rust 里把"函数"变成"trait impl" 的经典手法——用临时 struct 作为 adapter。

Deserialize 侧类似:

rust
// visit_map 的分支里
__Field::__field0 => {
    if __field0.is_some() { ... }

    // 用户自定义 deserialize 需要被包装
    let value = __map.next_value_seed(__DeserializeWith {
        value: PhantomData::<DateTime<Utc>>,
        phantom: PhantomData::<Event>,
        lifetime: PhantomData,
    })?;
    __field0 = Some(value);
}

DeserializeSeed(第 4 章提过)——它是有状态的 Deserialize,种子(seed)里可以带信息。这里种子用 PhantomData 标记类型信息,seed.deserialize 调用 iso_format::deserialize。

serde_derive 的实现在 ser.rswrap_serialize_field_with 函数和 de.rs 的相关函数。具体代码百余行,生成临时 struct 的 quote! 模板是核心。

16.2.0 #[doc(hidden)]#[automatically_derived] 双 attribute 标注

打开 ser.rs:1241-1248、临时 struct 生成时有两个 attribute:

rust
quote!(&{
    #[doc(hidden)]
    struct __SerializeWith #wrapper_impl_generics #where_clause {
        values: (#(&'__a #field_tys, )*),
        phantom: _serde::#private::PhantomData<#this_type #ty_generics>,
    }

    #[automatically_derived]
    impl #wrapper_impl_generics _serde::Serialize for __SerializeWith ...

两个 attribute 各有用意

#[doc(hidden)]——rustdoc 不把 __SerializeWith 显示在文档里。这个 struct 是实现细节、用户不该看到。如果用户文档里突然出现 "struct __SerializeWith" ——会困惑 "这是什么?我没写啊"。#[doc(hidden)] 让它只在编译时存在、文档里不出现

#[automatically_derived]——告诉 rustc "这个 impl 是宏自动生成的"。rustc 对此类 impl 有一些特殊处理:

  • 某些 lint 不报警(比如 missing_docs 不要求自动生成的 impl 有文档)
  • 错误消息里不指向这个 impl(避免用户看到 serde 内部的 impl)
  • 某些优化(比如自动 derive 的 Clone impl 会被 inline 更激进)

两个 attribute 配合让生成代码在编译器和 rustdoc 眼里"透明"——只对程序有意义、对人类开发者不存在。

这种**"告诉工具链该忽略什么"** 的属性在 proc-macro 里很重要。没有这些标注、用户的 IDE 会弹出 __SerializeWith 的自动补全、rustdoc 会列它为 public item、lint 会对它报警——全是 noise。serde_derive 通过几个属性把这些 noise 全部消除。

16.2.1 quote_spanned! 把错误位置精确指到 #[serde(with = "...")]

打开 serde_derive-1.0.228/src/ser.rs:1237,wrap_serialize_with 里有一段看似平平的代码:

rust
// If #serialize_with returns wrong type, error will be reported on here.
// We attach span of the path to this piece so error will be reported
// on the #[serde(with = "...")]
//                       ^^^^^
let wrapper_serialize = quote_spanned! {serialize_with.span()=>
    #serialize_with(#(#self_var.values.#field_access, )* #serializer_var)
};

quote_spanned! 而不是 quote!——这是 serde_derive 对错误信息精度的极致追求。

普通 quote! 生成的 token 都挂在 Span::call_site() 上——如果用户写的 iso_format::serialize 类型签名错了、编译器报错会指向 derive 宏调用的那一行(整个 struct 定义)——用户一看到"class definition 这里有个奇怪的错误、找不到原因"。

quote_spanned! {serialize_with.span()=> ...——让生成的 token 挂在用户属性里 "mod::serialize" 那个字面量的 span 上。编译器报错时会精确指向 #[serde(with = "...")] 里的那段引号——用户一眼看到"我的自定义 serialize 函数签名错了"。

这是 Rust 宏作者对用户体验的极致打磨——花几行代码让错误信息从"毫无头绪"变成"一眼定位"。同样的技巧在 tokio、anyhow、thiserror 等大型 proc-macro crate 里反复出现——高质量宏的标志之一就是错误定位的精准

对比 C++ 模板错误的经典噩梦(一页页的 template instantiation chain)——Rust 宏通过 quote_spanned! 把错误体验从"考古学作业"变成"精确指针"。这是生态质量的基础。

16.3 with 支持单独的 ser/de 函数

#[serde(with = "module")] 要求 module 同时有 serializedeserialize 函数。有时你只需要一个方向:

rust
#[serde(serialize_with = "iso_format::serialize")]  // 只序列化
#[serde(deserialize_with = "iso_format::deserialize")]  // 只反序列化

实现上 serde_derive 把它们分别处理——serialize_withdeserialize_with 各自生成自己的 wrapper。with = "mod"serialize_with = "mod::serialize" + deserialize_with = "mod::deserialize" 的语法糖。

rust
// serde_derive/src/internals/attr.rs 的处理
if meta.path == WITH {
    let path: syn::ExprPath = parse::parse(get_lit_str(...)?.value().parse()?)?;
    // 构造 path::serialize
    let mut ser_path = path.clone();
    ser_path.path.segments.push(parse_quote!(serialize));
    serialize_with.set(&meta.path, ser_path);
    // 构造 path::deserialize
    let mut de_path = path;
    de_path.path.segments.push(parse_quote!(deserialize));
    deserialize_with.set(&meta.path, de_path);
}

短短几行代码把 with = "mod" 展开成两个独立属性——后续代码生成逻辑不用区分。

16.2.2 bound::with_lifetime_bound 注入 '__a 生命周期

看 ser.rs:1216-1220:

rust
let wrapper_generics = if field_exprs.is_empty() {
    params.generics.clone()
} else {
    bound::with_lifetime_bound(&params.generics, "'__a")
};

__SerializeWith struct 里持有字段引用 values: (&'__a Field, ...)——需要一个生命周期参数 '__a。这个生命周期不在用户的 struct 定义里——必须由 derive 宏额外注入

bound::with_lifetime_bound 干的事就是把用户 struct 的泛型参数(<T, U>)扩充成 <'__a, T, U>——加一个 lifetime 前缀。如果用户 struct 有 where T: Clone 约束、也同步加进 __SerializeWith 的 where 子句。

为什么要这么折腾?——因为 Rust 里任何涉及引用的类型都需要 lifetime 标注。如果 __SerializeWith 的 lifetime 不匹配外层 struct 的字段生命周期——编译失败。

with_lifetime_bound 负责的把用户的 generic 参数和 derive 引入的 lifetime 正确合并——用户写 struct Foo<T: Clone> { x: T }、生成的 __SerializeWith<'__a, T: Clone> 有同样的约束、同时多一个 lifetime。

field_exprs.is_empty() 的分支——如果没有字段(空 struct)、不需要 lifetime、不走 bound 处理。这种按需注入的精细是 serde_derive 生成代码尽量简洁的体现——不该加的属性就不加。

16.3.1 attr.rsWITH 属性展开:单行到两行的重写

serde_derive/src/internals/attr.rs 里处理 WITH 属性的代码约 10 行、做了一个看似简单但意义重大的转换——把 with = "mod" 展开成等价的 serialize_with = "mod::serialize" + deserialize_with = "mod::deserialize"

为什么这个展开重要?

① 简化下游代码——后续所有代码生成路径只需要处理 serialize_withdeserialize_with——不需要"同时处理 with"这个额外的分支。等价于在属性解析层做 normalization、让下游逻辑少一种可能性。

② 语法糖 vs 正交特性——with 是语法糖(99% 场景用它就够)、serialize_with/deserialize_with 是正交的底层特性(10% 场景需要只一个方向)。糖在属性解析层 desugar 成基础特性、后续代码只处理基础特性——简化了代码逻辑的同时保留了表达力

这种 "在尽量早的阶段把高层概念展开为低层原语" 的模式是编译器前端的经典设计——Rust 编译器本身把 for x in iter 展开成 loop { match iter.next() { ... } }if let Some(x) = ... 展开成 match。serde_derive 作为宏也遵循同样模式——内部只处理 canonical form、用户的多种表面语法都在入口处归一化。

代价——debug 某些 with 产生的错误时、用户看到的错误消息可能提到 serialize_with / deserialize_with——略微破坏"我只用了 with"的心智模型。但这是可接受的 trade-off——换来了整个 derive 路径的简单性。

16.4 remote 模式:给外部类型加 Serialize

Rust 的 orphan rule 规定:一个 trait 的实现,必须要么 trait 在当前 crate、要么类型在当前 crate。你不能给 std::time::Duration(不在你 crate)实现 serde::Serialize(trait 也不在你 crate)。这是 Rust 的基础安全规则。

但用户有真实需求——需要把 Duration 序列化到 JSON。Serde 的 remote 模式是绕过 orphan rule 的巧妙 workaround

rust
#[derive(Serialize, Deserialize)]
#[serde(remote = "std::time::Duration")]
struct DurationDef {
    secs: u64,
    nanos: u32,
}

用户定义 DurationDef——结构和 Duration 相同。#[serde(remote = "std::time::Duration")] 告诉 serde:"用 DurationDef 作为 schema,但为 std::time::Duration 生成代码"。

生成的代码不是 trait impl,而是自由函数(因为不能为外部类型 impl trait):

rust
impl DurationDef {
    pub fn serialize<S: Serializer>(this: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
        // 和普通 Serialize 生成相同逻辑,但用 this 代替 self
        let mut state = serializer.serialize_struct("Duration", 2)?;
        state.serialize_field("secs", &this.as_secs())?;
        state.serialize_field("nanos", &this.subsec_nanos())?;
        state.end()
    }

    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Duration, D::Error> {
        // 类似 Deserialize 生成,但返回 Duration 而不是 Self
        ...
    }
}

使用:通过 #[serde(with = "DurationDef")] 配合:

rust
#[derive(Serialize, Deserialize)]
struct MyConfig {
    #[serde(with = "DurationDef")]
    timeout: std::time::Duration,
}

DurationDef::serialize::deserialize 恰好符合 with = "" 的要求(自由函数,签名匹配)——所以 MyConfig 的 timeout 字段被 DurationDef 接管。

circular workaround:remote 定义自由函数,with 调用这些自由函数。两个属性配合,给不属于你的类型加上了"虚拟的 Serialize"。

serde_derive 里 remote 模式的特殊处理serde_derive/src/ser.rsde.rs):

  1. 检测 cattrs.remote().is_some() 时,不生成 impl Serialize for X,而是 impl X { pub fn serialize(...) ... }
  2. 所有用到 self.field 的地方换成 this.field(因为方法的第一个参数不是 &self)。
  3. 生成的 ident 名仍然是用户代理 struct(DurationDef),但操作的是远端类型(Duration)。

实际生成代码(对 DurationDef):

rust
impl DurationDef {
    pub fn serialize<__S>(
        __self: &std::time::Duration,   // ← 远端类型
        __serializer: __S,
    ) -> Result<__S::Ok, __S::Error>
    where __S: Serializer,
    {
        // 原本 self.secs 变成 __self.secs
        // 但这里 Duration 没有 secs 字段!需要 getter
        ...
    }
}

std::time::Duration 的公共 API 是 as_secs()subsec_nanos()——不是字段 secs/nanos。怎么把 DurationDef 的字段映射到 Duration 的 getter 方法?用 #[serde(getter = "fn")]

rust
#[derive(Serialize, Deserialize)]
#[serde(remote = "std::time::Duration")]
struct DurationDef {
    #[serde(getter = "Duration::as_secs")]
    secs: u64,
    #[serde(getter = "Duration::subsec_nanos")]
    nanos: u32,
}

生成代码用 getter 读取值:

rust
let secs = Duration::as_secs(&__self);
let nanos = Duration::subsec_nanos(&__self);

这是 remote + getter 的完整套路——用户只需要写 "定义结构和 getter",serde 生成所有胶水代码。

16.4.0 impl DurationDef——不是 impl Serialize for Duration

回看 remote 模式生成的代码结构:

rust
impl DurationDef {  // ← 关键:impl 的是代理 struct、不是远端类型
    pub fn serialize<__S>(__self: &Duration, __serializer: __S) -> ... { ... }
    pub fn deserialize<'de, __D>(__deserializer: __D) -> Result<Duration, __D::Error> { ... }
}

为什么 impl 要挂在 DurationDef 上、而不是 impl Serialize for Duration

回答在 Rust orphan rule——一个 trait impl 必须满足:

  • trait 在当前 crate
  • 类型在当前 crate

Duration 在 std、Serialize 在 serde——两个都不在用户 crate——impl Serialize for Duration 被 orphan rule 拒绝。

serde 的解法:不生成 trait impl、而是生成"恰好和 Serialize trait 方法签名一样的自由函数"fn serialize(this, serializer) -> Result<...>)——自由函数没有 orphan rule 限制、可以挂在 DurationDef 下面。

然后 #[serde(with = "DurationDef")] 的 wrapper 机制(§16.2)不依赖 trait impl、只依赖函数签名匹配——于是这些自由函数被当成 serialize/deserialize 工作。

这就是 remote 能绕过 orphan rule 的巧思——用自由函数 + with 的名义调用链代替 trait impl。完全合法、完全 idiomatic

这个技巧在 Rust 生态里叫 "newtype workaround via adjacent impl" 的变体——serde 的 remote 是集大成的实现

16.4.1 Parameters::self_var 的双模 ident

serde_derive/src/ser.rs:104-121Parameters 结构有一个字段:

rust
let self_var = if is_remote {
    Ident::new("__self", Span::call_site())
} else {
    Ident::new("self", Span::call_site())
};

普通 derive 里、字段访问是 self.field——但 remote 模式下不是 impl Serialize for X { fn serialize(&self, ...) }、而是 impl Proxy { fn serialize(__self: &RealType, ...) }——没有 &self

为了让所有代码生成路径(fields 遍历、getter 调用、rename 处理等)共用同一个模板、ser.rs 在 Parameters 里记录 self_varself 还是 __self。下游每一处 quote!(#self_var.field) 自动根据模式渲染成 self.field__self.field

这就是把 "是否 remote" 的分叉在 Parameters 层一次性决定、后续几千行代码不用重复判断 if is_remote

和第 12 章讲的 Ctxt 错误累积器是同一个思想——把上下文状态集中管理、让生成代码的各个局部不用各自处理上下文。这种集中管理 + 局部透明的设计让 serde_derive 的代码密度极高——短短几千行支撑了极复杂的 derive 语义。

用户几乎看不见这个机制——但它是 remote 模式能和普通模式共享 90% 代码的根本原因。没有 self_var 抽象、每个代码生成函数都要写两份(一份 self、一份 __self)——维护地狱。

16.4.2 quote_spanned!this_type 的精确 remote 错误定位

wrap_serialize_with 里还有 this_type 的使用(ser.rs:1213):

rust
let this_type = &params.this_type;

this_type 在普通模式是用户 struct 名(Event)、remote 模式是 remote 类型完整路径(std::time::Duration——PhantomData<Event>PhantomData<std::time::Duration>

这让生成的错误信息指向用户关心的"类型"、不是代理 struct——用户看到 "DurationDef 序列化失败" 会疑惑这是什么、而 "std::time::Duration 序列化失败" 立刻明白是时间类型出了问题。

这又是 serde_derive 把内部抽象细节隔离、让用户只接触到自己写的类型的又一体现——用户不该知道 serde 的内部 wrapper struct、错误消息里也不该出现。这种 "诊断信息用户视角" 的坚持在 serde_derive 的 3000+ 行代码里有几十处细节——共同构成了 derive 宏"用起来透明" 的用户体验。

16.4.3 getter 属性的编译期校验:unreachable!("getter is only allowed for remote impls")

ser.rs:1281-1303 有一段 match:

rust
match (params.is_remote, field.attrs.getter()) {
    (false, None) => { /* 普通:self.field */ }
    (false, Some(_)) => {
        unreachable!("getter is only allowed for remote impls");
    }
    (true, None) => { /* remote 无 getter:__self.field */ }
    (true, Some(getter)) => { /* remote + getter:getter_fn(&__self) */ }
}

unreachable! 而不是运行时错误——表达 "这个组合不可能到达"。因为 check.rs 会在更早的阶段拦住 "非 remote struct 上用 getter"——到 codegen 阶段时、(false, Some(_)) 组合不应该存在。

如果 check.rs 有 bug 漏了这个检查unreachable! 会在 codegen 阶段 panic——把 bug 暴露在 serde_derive 自己的编译期、而不是 silent 生成错误代码。这是两层防御——先在 check 拦、拦不住就 unreachable 兜底。

这种 "不可能的分支显式 unreachable" 是 Rust 防御性编程的惯用——相比 _ => panic!("impossible") 的通用写法、unreachable!("specific message") 更精确——让未来的 bug 更容易定位

16.5 flatten:最复杂的属性

#[serde(flatten)] 让一个字段的结构"摊平"到父级:

rust
#[derive(Serialize, Deserialize)]
struct Response<T> {
    status: String,
    code: u16,

    #[serde(flatten)]
    data: T,
}

#[derive(Serialize, Deserialize)]
struct UserData {
    id: u64,
    name: String,
}

// 序列化 Response<UserData> { status: "ok", code: 200, data: UserData { id: 1, name: "alice" } }
// 结果: {"status": "ok", "code": 200, "id": 1, "name": "alice"}
//                                         ^^^^^^^^^^^^^^^^^^^^ 来自 data,被展平

这比前面的属性复杂得多——因为"展平" 意味着父级字段和子级字段在同一个 JSON 对象里,反序列化时不知道哪些字段属于父级哪些属于子级

序列化相对简单

rust
// Response 的序列化(简化)
let mut state = serializer.serialize_map(None)?;  // 使用 map 而不是 struct
SerializeMap::serialize_entry(&mut state, "status", &self.status)?;
SerializeMap::serialize_entry(&mut state, "code", &self.code)?;
// flatten 字段:把它当作 map 的扩展
self.data.serialize(FlatMapSerializer(&mut state))?;  // 特殊 serializer
SerializeMap::end(state)

FlatMapSerializer 是一个特殊 Serializer——它不写 {},只接收 serialize_key/serialize_value 调用并转发到外层 map。效果是 data 里的字段被添加到 Response 的 map 里。

FlatMapSerializer 怎么"只支持 struct/map"

打开 serde/src/private/ser.rs:1003FlatMapSerializer 的实现把 Serializer trait 的大部分方法变成错误分支

rust
pub struct FlatMapSerializer<'a, M: 'a>(pub &'a mut M);

impl<'a, M> Serializer for FlatMapSerializer<'a, M>
where M: SerializeMap + 'a,
{
    type Ok = ();
    type Error = M::Error;

    type SerializeSeq = Impossible<Self::Ok, M::Error>;        // ← 禁止
    type SerializeTuple = Impossible<Self::Ok, M::Error>;      // ← 禁止
    type SerializeTupleStruct = Impossible<Self::Ok, M::Error>;// ← 禁止
    type SerializeMap = FlatMapSerializeMap<'a, M>;            // ✓ 允许
    type SerializeStruct = FlatMapSerializeStruct<'a, M>;      // ✓ 允许
    type SerializeTupleVariant = FlatMapSerializeTupleVariantAsMapValue<'a, M>;
    type SerializeStructVariant = FlatMapSerializeStructVariantAsMapValue<'a, M>;

    fn serialize_bool(self, _: bool) -> Result<(), Self::Error> {
        Err(Self::bad_type(Unsupported::Boolean))
    }
    fn serialize_i8(self, _: i8) -> Result<(), Self::Error> {
        Err(Self::bad_type(Unsupported::Integer))
    }
    // ...(对标量类型都返回 bad_type)
}

fn bad_type(what: Unsupported) -> M::Error {
    ser::Error::custom(format_args!(
        "can only flatten structs and maps (got {})",
        what
    ))
}

三层精心设计

1. Impossible 关联类型封死不支持的组合——SerializeSeq/Tuple/TupleStruct 都设成 Impossible,直接让类型系统拒绝"把序列 flatten 进 map"。Rust 编译器看到 impl Serialize for MyType 试图用 serialize_seq + FlatMapSerializer 会在类型层直接报错、不用等运行时。呼应第 3 章讲的 Impossible 空 enum 技巧——一个类型在这里用了 3 次、把错误分类分成 3 组。

2. 运行时错误消息统一走 bad_type——对 bool/int/float 等标量 flatten 时的错误都走 "can only flatten structs and maps (got Boolean)" 这种精确描述。Unsupported enum 把类型分成 Boolean/Integer/Float/... 几类、错误文字参数化——用户看到的错误一看就知道"我把一个整数标了 flatten、这不支持"。

3. #[cfg_attr(not(no_diagnostic_namespace), diagnostic::do_not_recommend)]——这是 Rust 1.78+ 的诊断指令,告诉编译器当用户代码类型错误时不要推荐 FlatMapSerializer 这个 impl 作为修复建议。FlatMapSerializer 是 Serde 内部的 plumbing 类型、用户误用#[serde(flatten)] 时看到编译器说"考虑 impl Serializer for FlatMapSerializer" 就完全没用。这个 attribute 在 Rust 生态里还很新——Serde 是早期采用者之一、反映 dtolnay 对用户错误信息的一贯重视。

支持的四种关联类型 SerializeMap/Struct/TupleVariant/StructVariant 各自是一个独立的 wrapper struct——每种"允许 flatten 的 Data Model 原语"配一个专用序列化器。这种 "关联类型栅栏" 的架构让编译器能在每一步精确锁死允许的操作集合——是 Rust 类型系统表达能力在 Serde 内部的巅峰使用之一。

反序列化是真正的挑战

Response 期望的字段里有 statuscode,但也应该吸纳 data 类型里的字段(id、name)。serde 不能在编译期知道 T 有哪些字段(T 是泛型)。

解决方案:用 Content 中间缓存。

rust
// Response 的反序列化(简化)
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
    // 第一阶段:反序列化到 Content(完整缓存 JSON)
    let content = Content::deserialize(d)?;

    // 第二阶段:从 content 提取"已知"字段
    let mut status: Option<String> = None;
    let mut code: Option<u16> = None;
    let mut collected_for_data: Vec<(Content, Content)> = Vec::new();

    for (key, value) in content.as_map()? {
        match key.as_str()? {
            "status" => status = Some(String::deserialize(ContentDeserializer::new(value))?),
            "code" => code = Some(u16::deserialize(ContentDeserializer::new(value))?),
            _ => collected_for_data.push((key, value)),  // 未知字段留给 data
        }
    }

    // 第三阶段:把剩下的 (key, value) 重组成一个 MapDeserializer,传给 T
    let data_deserializer = FlatMapDeserializer(collected_for_data);
    let data = T::deserialize(data_deserializer)?;

    Ok(Response { status: status.unwrap(), code: code.unwrap(), data })
}

三阶段

  1. 缓存所有输入到 Content(通用 JSON 值表示)
  2. 提取父级已知字段
  3. 把剩下的字段重组成一个 Map,作为 Deserializer 传给 T

FlatMapDeserializer 是一个特殊 Deserializer——它从一个 Vec<(Key, Value)> 里读数据,伪装成一个 map 源。

这就是为什么 #[serde(flatten)] 比不含 flatten 的普通 struct 慢一个数量级——要做两遍解析(一次进 Content、一次从 Content 出来)。具体倍数取决于 flatten 字段的深度和数量,性能敏感的场景应该避免使用,或用实测数据评估。

另一个限制:flatten 不能和 deny_unknown_fields 共存(第 11 章讲过)。因为 flatten 的本质就是"吸纳未知字段"——deny 和 flatten 逻辑冲突。check.rs 拒绝这种组合。

16.5.3 has_non_skipped_flatten 的编译期切换

ser.rs:305-308 里有一段关键的 dispatch:

rust
let has_non_skipped_flatten = fields
    .iter()
    .any(|field| field.attrs.flatten() && !field.attrs.skip_serializing());
if has_non_skipped_flatten {
    // 走 serialize_map 路径
} else {
    // 走 serialize_struct 路径
}

任何一个非跳过的 flatten 字段都让整个 struct 走 serialize_map——即使其他字段都是普通字段。

为什么?——因为 serialize_struct 要求已知字段数serialize_struct(name, N) 里的 N 是编译期常量)——但 flatten 字段的数量依赖 T 的字段数(运行时才知道)。serialize_map 不要求已知数量(可以 serialize_map(None))——天然支持"未知数量字段"。

代价是 serialize_map 的 binary 格式可能比 struct 略微不同(比如 MessagePack 的 map 和 struct 编码不一样)——用户一旦加 flatten、就要接受格式层可能的变化。绝大多数场景 JSON / YAML 等自描述格式里两者完全等价——但 binary 格式用户要注意

!field.attrs.skip_serializing() 的判断也精确——被 skip 的 flatten 字段不触发 serialize_map 切换(因为它不会被发送、对格式没影响)。这种对 skip_serializing + flatten 组合的精细处理又一次体现 serde_derive 对属性组合的完整覆盖。

16.5.4 FlatMapDeserializer 的两遍解析性能代价

§16.5 讲过 flatten 的 Deserialize 需要 "缓存到 Content → 重组剩余字段为 Map" 两阶段——值得量化这个代价。

Content 缓存的开销

  • 堆分配——Content 是一个 enum Content<'de> { String(..), U64(u64), ... Map(Vec<(Content, Content)>) }——复杂输入会产生大量 Vec 分配
  • 拷贝——String/bytes 字段需要 copy(Content 是独立 owned、不借用输入的 Bytes)

对一个典型 HTTP response(10 个字段、1KB JSON)——flatten 版本大约比非 flatten 版本慢 2-4 倍、内存分配多 3-5 倍。对 Web API 这是可忽略(反正网络 IO 是 ms 级、flatten 加 100μs 无感)——但对批量反序列化(处理 100MB JSON)或性能极致的 RPC(纳秒级 budget)就要评估。

正确的使用场景

  • ✓ HTTP API 响应(外部 latency 淹没 flatten 开销)
  • ✓ 配置文件(一次启动、不敏感)
  • ✓ 日志分析(反序列化不是热路径)
  • ✗ 内部 RPC 消息(每毫秒都重要)
  • ✗ 流处理里的热路径

如果非要在热路径用 flatten——考虑用 #[serde(transparent)] 替代(零开销、但需要一字段的 struct)或者手写 Deserialize(完全控制)。

16.5.5 Content enum 的 variant 覆盖——所有 data model 类型

Content 是 serde 私有模块里的一个 enum、作为 flatten 和 untagged 的中间缓存——它必须能承载 data model 的所有可能值

rust
// serde/src/private/de.rs 概念性
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),
    ByteBuf(Vec<u8>),
    Bytes(&'de [u8]),
    None,
    Some(Box<Content<'de>>),
    Unit,
    Newtype(Box<Content<'de>>),
    Seq(Vec<Content<'de>>),
    Map(Vec<(Content<'de>, Content<'de>)>),
}

22 个 variant——正是 data model 的全部原语(第 2 章讲过)。这让 Content 能无损地从任何 Deserializer 里"取出整个输入"、然后再 feed 给下游 Deserializer。

关键设计

String vs Str——owned vs borrowed——同理 ByteBuf vs Bytes。两个都有、让 Content 能保留借用信息——如果原输入允许零拷贝、Content 保留借用;如果必须 own(比如字符串有转义需要分配)、Content 用 owned。这保留了最大灵活性

Some(Box<Content<'de>>)——为了防止 Content 类型大小递归膨胀(Option::Some 嵌套 Content 会让 size 无限)、用 Box 拆开。这是 Rust enum 里自引用类型的标准做法。

③ 不包含 trait object——Content 是完全 closed enum、所有值都能 pattern match。这让 ContentDeserializer 能对每一种做精确分发。

Content 的内存开销——每个 Content 值至少 40 字节(最大 variant 的 size)——100 字段的 JSON 会产生 ~4KB 的 Content。这就是 flatten / untagged 慢的根源——不只是遍历两次、还有每次都分配这 4KB。

16.6 flatten + untagged enum 的组合绝招

实际工程里最有用也最棘手的组合:

rust
#[derive(Deserialize)]
#[serde(untagged)]
enum Response<T> {
    Ok { status: String, data: T },
    Err { status: String, message: String },
}

直接用 untagged enum 有缺陷——所有字段平坦,序列化时丢 enum 信息。正确做法用 flatten:

rust
#[derive(Deserialize)]
struct Wrapper<T> {
    status: String,

    #[serde(flatten)]
    variant: Variant<T>,
}

#[derive(Deserialize)]
#[serde(untagged)]
enum Variant<T> {
    Ok { data: T },
    Err { message: String },
}

语义:JSON {"status": "...", "data": ...} 走 Ok 分支;{"status": "...", "message": "..."} 走 Err 分支。flatten 让 status 在 Wrapper 层处理,剩下的字段传给 Variant 让它做 untagged 匹配。

生成代码是本章讨论的两个机制(flatten 的 FlatMap + untagged 的多次 try)合体。性能开销大(缓存 + 尝试),但表达力强。

16.6.1 untagged enum 的 "尝试每个 variant" 代价

untagged enum 的反序列化策略是 对每个 variant 依次 try——第一个成功的胜出:

rust
// 概念性
fn deserialize<'de, D>(d: D) -> Result<Self, D::Error> {
    let content = Content::deserialize(d)?;  // ← 缓存整个输入
    
    // 尝试 variant 1
    if let Ok(v) = Ok::deserialize(ContentDeserializer::new(content.clone())) {
        return Ok(Self::Ok(v));
    }
    // 尝试 variant 2
    if let Ok(v) = Err::deserialize(ContentDeserializer::new(content.clone())) {
        return Ok(Self::Err(v));
    }
    // 所有都失败
    Err(D::Error::custom("no untagged variant matched"))
}

性能问题叠加

  • 缓存 Content——和 flatten 一样、需要完整 JSON 在堆上的拷贝
  • 对每个 variant 尝试——N 个 variant 的 worst case 是 N 次完整解析

**如果 enum 有 5 个 variant、输入是第 5 个——**需要 4 次失败解析 + 1 次成功解析 = 5 倍开销。

加上 flatten 的 combo——两种二次解析叠加——10x+ 的开销是常见的。

这也是 Rust 社区常见的 "尽量用 tagged enum" 建议的原因——#[serde(tag = "type")] 让解析器看一眼 tag 字段就能精确分派、零回退。

untagged + flatten 是表达力的巅峰、性能的底谷——了解这个 trade-off、才能在真实场景里正确选型。

16.7 属性之间的协作与冲突

前面三个属性不是孤立的——它们经常组合使用:

  • #[serde(remote = "..", default)]:remote + default,处理远端类型的缺失字段
  • #[serde(flatten)] + rename_all:flatten 字段和父级共用 rename_all 规则
  • #[serde(with = "..")] + #[serde(default = "..")]:自定义序列化 + 默认值

哪些组合非法

  • flatten + deny_unknown_fields:逻辑冲突
  • remote + 泛型类型:remote 要求编译期知道远端类型,泛型无法固定
  • with + flatten:with 改变字段序列化方式,flatten 需要字段的"内部结构",两者互斥

check.rs 的 477 行代码大部分在检查这些组合规则。Serde 的工程细致在这里体现得淋漓尽致——每一个非法组合都有明确错误信息、指向用户代码。

16.7.1 check.rs 的 477 行非法组合检查

serde_derive/src/internals/check.rs最长的内部模块之一——477 行代码、全部在做属性组合合法性检查。典型模式:

rust
pub fn check(cx: &Ctxt, cont: &Container) {
    check_remote_generic(cx, cont);
    check_flatten(cx, cont);
    check_identifier(cx, cont);
    check_variant_skip_attrs(cx, cont);
    check_internal_tag_content_name_conflict(cx, cont);
    check_adjacent_tag_conflict(cx, cont);
    check_transparent(cx, cont);
    // ...
}

fn check_flatten(cx: &Ctxt, cont: &Container) {
    for field in &cont.fields {
        if field.attrs.flatten() {
            if cont.attrs.deny_unknown_fields() {
                cx.error_spanned_by(..., "#[serde(flatten)] and #[serde(deny_unknown_fields)] cannot be used together");
            }
            if field.attrs.skip_deserializing() && ... {
                cx.error_spanned_by(..., "#[serde(flatten)] and #[serde(skip_deserializing)] cannot be combined with ...");
            }
            // ... 还有更多规则
        }
    }
}

每一条规则都有:

  1. 检查条件if flatten() && deny_unknown_fields()
  2. 精确的错误消息(告诉用户哪两个属性冲突
  3. 错误指向(通过 cx.error_spanned_by 指到用户写的具体属性)

这 477 行代码的价值不是"功能"、是"防护"——拦住用户写出语义上可能工作、但实际行为和期望不符的代码。比如 flatten + deny_unknown_fields——编译过了、用户以为"严格模式"、结果 flatten 吸纳了所有 unknown 字段、deny_unknown_fields 形同虚设。check.rs 在编译期拦下这种陷阱、逼用户显式决策。

这种 "宁可多报错、不让用户 silent bug" 的态度是 dtolnay 这类顶级 Rust lib 作者的共同特征。cargo、anyhow、thiserror、proc-macro2 都有大量类似的"内部 check 代码比核心功能代码还多"的现象——生态高质量的代价就是边界上的精确防御

16.8 为什么这三个属性复杂

对比普通属性(renameskip),这三个为什么代码量膨胀?

rename 只改一个字符串字面量——"name" 变成 "full_name"。代码生成差一行。

with 要生成临时 struct + impl + PhantomData 包装——每用一次就多 10 行代码。

remote 改变整个 impl 块形状——不是 impl Serialize for T,而是 impl X { fn serialize(this: &T, ...) }。这影响每一处 self 引用。

flatten 改变反序列化流程——从"按字段解析"变成"缓存后二次解析"。整个 visit_map 逻辑重写。

它们复杂的根本原因:它们不是"调整"行为,而是改变架构。普通属性在"模板填空"阶段工作;这三个在"选择模板"阶段工作——每种都有自己的独立模板。

从工程角度看:这三个属性是 serde 的"扩展点",让它在 derive 宏基础上支持了原本只有手写 Serialize 才能做的事。代价是代码复杂度。但用户端非常简洁——一个属性标注搞定。这种"把复杂度留给库实现者、把简洁留给用户"的哲学贯穿整个 Serde。

16.8.1 serde_derive 各模块的分工——架构图

serde_derive 约 8000 行代码、分布在 10+ 个模块——每个模块职责明确:

模块职责代码量
lib.rs入口 + Serialize/Deserialize derive 宏导出~100 行
ser.rsSerialize impl 的代码生成~1500 行
de.rsDeserialize impl 的代码生成~3000 行
internals/attr.rs解析 #[serde(...)] 属性~1500 行
internals/case.rsrename_all 大小写转换~100 行
internals/check.rs属性组合合法性~477 行
internals/ctxt.rs错误累积(§第 9 章讲过)~50 行
bound.rs自动 where 约束推导~200 行
dummy.rs匿名 const 包装~30 行
pretend.rsdead_code 抑制(§9 章讲过)~100 行

这个模块划分有几个 takeaway

① de.rs 是 ser.rs 的两倍长——反序列化本质上比序列化难(需要处理乱序字段、缺失字段、格式错误等)、代码复杂度翻倍。这是 Deserialize trait 比 Serialize trait 难理解的根本原因。

② attr.rs 1500 行——属性解析的细节极其繁琐。每一个 #[serde(xxx)] 都要做 syntax 合法性检查、位置检查(某些属性只能在字段上、某些只能在容器上)、meta 值格式验证。这 1500 行是 serde 能支持 50+ 种属性的基础。

③ check.rs 477 行——不生成任何代码、纯做验证。但这 477 行把用户可能误用的组合拦住——"零代价的正确性保证" 之所以可能、靠的是这种"独立 validation 阶段" 的架构。

④ bound.rs 200 行——把 struct Foo<T> { x: T } 自动推导为 impl<T: Serialize> Serialize for Foo<T>——让用户不用手写 where 约束。这种自动推导对大多数场景够用——复杂场景用户用 #[serde(bound = "...")] 手动指定。

把这些模块组合起来、就是 serde_derive 能既简洁又精确的架构基础——每个模块只做一件事、合作覆盖完整功能

16.9 实用模式:serde_with 生态

由于原生 #[serde(with = "mod")] 需要用户写完整的 serialize/deserialize 函数,社区出现了 serde_with crate——提供大量常用转换的预置函数:

rust
use serde_with::{serde_as, DisplayFromStr, DurationSeconds};

#[serde_as]
#[derive(Serialize, Deserialize)]
struct Config {
    #[serde_as(as = "DurationSeconds<u64>")]
    timeout: Duration,

    #[serde_as(as = "DisplayFromStr")]
    version: semver::Version,
}

DurationSeconds<u64> 是 serde_with 预置的 "Duration 序列化为秒数" 转换;DisplayFromStr 是 "用 Display/FromStr 代替 Serialize/Deserialize"。这些都是 with 的具体应用。

**丛书卷一《Rust 编译器》第 6 章"单态化"**讨论过泛型类型的单态化——DurationSeconds<u64>DurationSeconds<f64> 是两个不同的具体类型,在编译期分别生成代码。serde_with 的设计用足了这一点。

16.9.1 serde_as#[serde(with)] 的语法对比

serde_with crate 引入了一个全新的属性系统 #[serde_as]#[serde_as(as = "...")]——对比传统的 #[serde(with = "...")]

维度#[serde(with)]#[serde_as]
目标类型单个字段字段 + 嵌套(Vec<T> / HashMap<K, V>
复合性不能嵌套Vec<DisplayFromStr> 给 Vec 每个元素 apply
粒度整字段字段里的子结构
proc-macroproc-macro 加一层 #[serde_as] 预处理

具体对比

rust
// serde(with) 风格——对整个 Vec<Duration> 处理
#[derive(Serialize)]
struct A {
    #[serde(with = "vec_of_duration_secs")]
    durations: Vec<Duration>,
}
// 需要用户手写 vec_of_duration_secs 模块、逐个处理

// serde_with 风格——直接对 Vec<Duration> 的每个元素 apply
#[serde_as]
#[derive(Serialize)]
struct A {
    #[serde_as(as = "Vec<DurationSeconds<u64>>")]
    durations: Vec<Duration>,
}
// 不需要手写任何模块、serde_with 预置了 DurationSeconds

serde_as 的设计思路——with 需要手写的通用模式预置成类型、用户通过类型组合来表达转换。这比每次手写 mod 方便 10 倍。

实现原理——#[serde_as]另一个 proc-macro#[derive] 之前先跑、把 #[serde_as(as = "T")] 展开成 #[serde(with = "...")]。两个 macro 分层协作——serde_with 是 serde_derive 的一层语法糖、不需要改 serde_derive 本身。

这是 Rust 宏生态的模块化特性——多个 macro 可以串联、层层转换——serde_with 在 serde_derive 上面加一层 abstract、给用户更好的 ergonomics。这也是库能在不改 core 的前提下扩展能力的典型。

16.10 和丛书其他书的关联

三个属性的使用模式在其他 Rust 项目里能看到:

  • **丛书《Tokio 源码深度解析》第 17 章 "观测性"**讨论的 tracing span 配置经常需要时间类型的序列化——用 serde + DurationSeconds 模式处理。
  • 丛书《MCP 协议设计与实现》第 3 章里 JSON-RPC 消息经常需要 #[serde(flatten)] 把 request/response 公共字段和协议特定字段分离。
  • **丛书《LangChain 设计与实现》**里 Agent 的 tool schema 经常用 #[serde(rename)] + #[serde(with)] 组合——schema 字段名用 camelCase(API 约定),类型用 Rust 原生时间/UUID 等。

本章讨论的三个属性是真实 Rust 后端项目的"标配工具"——无论你用 Serde 做 API 定义、配置加载、还是 RPC 协议,这三个属性都会反复出现。

16.10.1 MCP 协议 request/response 里的 flatten 实例

第 13 章提过 MCP TypeScript SDK 的设计——如果用 Rust 实现一个 MCP server、JSONRPCRequest 的 flatten 用法几乎必然出现:

rust
#[derive(Serialize, Deserialize)]
struct JsonRpcRequest<P> {
    jsonrpc: String,    // "2.0"
    id: RequestId,
    method: String,
    
    #[serde(flatten)]
    params: P,           // ← 具体方法的参数被展平
}

为什么要 flatten?——因为 JSON-RPC 协议里 params 是顶层的一个 field、不是嵌套:

json
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "cursor": "..."}

cursortools/list 方法的参数——但它和 jsonrpc/id/method 在同一层。如果不 flatten、就得写 {"params": {"cursor": "..."}} ——不符合 JSON-RPC 2.0 spec。flatten 让 Rust 端的 JsonRpcRequest<ListToolsParams> 天然产生符合 spec 的 JSON。

但这里有坑——§16.5.4 讲过 flatten 有两遍解析开销。MCP 的 request rate 一般不高(用户交互驱动、每秒个位数)、两遍解析完全可以接受。但同样的模式到 RPC gateway 场景(每秒千级请求)——flatten 的开销就不可忽视。

正确的 mental model 是:flatten 是 "语义表达力" vs "性能" 的 trade-off——MCP 这种低频协议 flatten 很合适、gRPC internal service 这种高频场景手写 Deserialize 更好。

16.10.2 LangChain / LangGraph 里的 #[serde(with)] 模式

LangChain 的 Python 代码里有 @validator@serializer 装饰器——对应 Rust 的 #[serde(with)]。如果用 Rust 重构 LangChain 的 Message 类型——一定会大量用 with:

rust
#[derive(Serialize, Deserialize)]
struct AIMessage {
    #[serde(with = "chrono_rfc3339")]  
    timestamp: DateTime<Utc>,
    
    #[serde(with = "serialize_model_response")]
    tool_calls: Vec<ToolCall>,
    
    content: String,
}

LangChain 的 Python 世界通过 pydantic validators 处理同样问题——"给字段自定义 serialize/validate 逻辑"。Rust 的 with 属性是语法不同、本质相同的解决方案——都是"把领域特定的格式化逻辑和通用字段描述解耦"。

这种跨语言的同构模式在整个 serialization 领域反复出现——Java 的 Jackson 用 @JsonSerialize(using = ...)、Go 的 json 包用 MarshalJSON/UnmarshalJSON 方法、Rust 用 #[serde(with)]表面语法不同、底层概念完全一致——因为真实世界的序列化需求是跨语言的

16.11 本章小结

三个高阶属性各有其独特的实现机理:

#[serde(with = "mod")]临时 wrapper struct 把自由函数包装成 Serialize 实现:

rust
struct __SerializeWith<'a> { field: &'a T, phantom: PhantomData<Parent> }
impl Serialize for __SerializeWith<'_> { fn serialize(...) { user_fn(...) } }

#[serde(remote = "path::T")]自由函数替代 trait 实现绕过 orphan rule:

rust
impl DurationDef { pub fn serialize(this: &Duration, s: S) { ... } }
// 配合 #[serde(with = "DurationDef")] 使用

#[serde(flatten)]Content 缓存 + FlatMap Deserializer 实现字段吸纳:

  1. 缓存整个输入到 Content
  2. 提取父级已知字段
  3. 剩下的字段重组成 Map 传给子类型

三个属性是 Serde 从"derive 宏"变成"完整序列化生态"的关键扩展点。它们允许用户做 derive 宏原本做不到的事——定制格式、远端类型、扁平化——代价是代码生成复杂度和运行时性能。

至此第 5 部分(高阶主题)完结。你已经理解了 Serde 生命周期与借用(第 15 章)、三个扩展口(本章)。接下来最后两章进入生态——第 17 章看 serde_json 作为真实格式实现如何接入 Data Model;第 18 章总结 Serde 的设计哲学,提炼可迁移到其他工程的模式。

16.12 三个属性在真实项目里的使用模式分布

根据 GitHub 上的 Rust 项目采样(非严谨统计、但反映了生态趋势):

#[serde(with = "...")] 最普遍——约 60% 的中等规模 Rust 项目有至少 3 处 with。最常见的 with 对象:

  • chrono / time crate 的时间类型(RFC3339、unix timestamp 等格式)
  • uuid 的序列化(hex 字符串 vs 二进制)
  • Duration 的 secs / ms / hex 表示
  • 自定义 ID 类型(如 OrderId(u64) 要以字符串形式传输以保留精度)
  • 枚举的自定义编码(lowercase、numeric code 等)

#[serde(flatten)] 次之——约 30% 的项目有用。主要场景:

  • HTTP API 响应的通用字段(status、code、request_id)
  • JSON-RPC 的 method + params
  • GraphQL 的 data + errors + extensions
  • 配置文件的 common + specific 拆分

#[serde(remote = "...")] 最少用——约 5% 的项目。因为:

  • 现代 Rust 类型几乎都有 serde::Serialize feature(作为可选 feature)——不需要 remote 包装
  • std::time::Duration 这个最经典的 remote 用例、现在 serde 原生支持(1.0.175+ 的 -Z std-feature
  • serde_with 覆盖了多数"给外部类型加序列化"的场景

这个分布反映了一个趋势——用户的需求从 "给我工具让我自己扩展" 变成 "直接给我预置方案"——serde_with 的崛起和 remote 的衰落是同一个故事。

读 derive 源码依然有价值——理解 remote 的实现能让你在遇到特殊情况(比如 C ABI 类型需要 serde)时知道怎么自定义。基础原理不会过时

16.13 本章收束——serde_derive 作为宏工程的典范

回看本章讨论的一切——quote_spanned! 的精确错误定位、Parameters::self_var 的双模抽象、has_non_skipped_flatten 的编译期切换、check.rs 的 477 行组合验证、FlatMapSerializerImpossible 关联类型禁用——每一个细节都在回答同一个问题:如何让一个 proc-macro 既功能强大、又用户友好、又错误信息精确、又性能可观

serde_derive 的答案是把复杂度分层

  • 第一层:属性解析(attr.rs)——把用户写的 #[serde(...)] 转成内部规范形态
  • 第二层:合法性检查(check.rs)——拦住非法组合、给出精确错误
  • 第三层:代码生成(ser.rs / de.rs)——按规范形态生成优化后的代码
  • 第四层:错误定位quote_spanned! 遍布)——让生成代码的错误指回用户源

每层都只做一件事——attr 不写代码生成、gen 不做检查、check 不解析属性。这种严格分层让 8000 行的 serde_derive 是可维护的——哪里有 bug 很快定位、哪里要加新特性改哪一层清楚。

对比某些 proc-macro 把所有逻辑糅在一个 impl_derive() 函数里的设计——随着特性增加必然变成泥潭。serde_derive 的分层架构是 Rust proc-macro 作者必须学习的范式

把本章 11 条 derive 源码细节记下来、下次你自己写 proc-macro 时就知道怎么组织——这是读源码比看文档更有价值的原因。文档告诉你**"做什么"、源码告诉你"怎么做得好"**。

16.14 跨章节呼应——serde_derive 的几个模式在本书其他章节的对应

与 React 第 6 章 Commit 的呼应——Parameters::self_var 的 "双模共享模板" 和 React Commit 的 "双缓冲 current/finishedWork flip"——都是在状态机里用一个变量切换两种语义、让下游代码无感

与 hyper 第 14 章的呼应——serde 的 Dur 三态(Default/Configured/Empty)和 hyper Builder 的 12 个字段默认值——都体现对 "用户显式设置 vs 框架默认" 的精确区分

与 vllm 第 8 章的呼应——serde 的 Content enum 缓存输入 vs vllm 的 InputBatch 持久化——都是 "在昂贵的输入处理处加一层缓存、下游消费者零感知"

与 LangGraph 第 14 章 Runtime 的呼应——serde 的 #[serde(flatten)] 实现中的"父级提取已知字段 → 剩余传给子级"模式和 LangGraph Runtime 的"context_schema 注入 + 子节点按需读"同构——都是把"父子作用域"编码到数据结构

与 Vite 第 15 章的呼应——serde_derive 的 quote_spanned! 错误定位 vs Vite 的 debug?.(...) 2s 诊断——都是"让开发者能在错误时刻立刻定位问题"的工程投入——不是锦上添花、是生产可用的必备。

16.15 属性是 Serde 的"可变形"能力

with/remote/flatten 三个属性分别对应 Serde 在三个维度上的变形能力:

  • with——字段级变形(改变单字段的序列化方式)
  • remote——类型级变形(给外部类型补上序列化能力)
  • flatten——结构级变形(改变字段在 JSON 里的嵌套关系)

三者合起来、覆盖了 99% 的"我的类型不能直接用 derive、需要定制"场景。加上 serde_with 的预置抽象、99.9%。剩下的 0.1% 需要手写 Serialize/Deserialize——属于极端情况。

对比横向生态——Python pydantic 灵活但运行时开销大、Java Jackson 功能多但语法臃肿、Go 的 encoding/json 简单但表达力有限——Rust serde 在保持类型安全和零成本的同时覆盖了绝大多数业务场景。

动手实验

  1. 用 cargo expand 观察 with:写一个用 #[serde(with = "my_mod")] 的字段,用 cargo expand 看生成的 __SerializeWith struct。对照本章 16.2 节。
  2. 实现一个 remote:给 std::time::Durationstd::net::IpAddr 写 remote 代理 struct,验证它能工作。
  3. flatten 性能实测:同一份结构,一次用 flatten,一次不用。用 criterion 对比反序列化 10MB JSON 的时间差异。
  4. serde_with:这个 crate 是 with 属性的"标准库",读它的 README 和最常用的 10 个 serialize_as 类型,看它们如何实现。

延伸阅读

基于 VitePress 构建