Serde 元编程

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

作者 杨艺韬 · 12,821 字 · 发布于 · 更新于

16.1 Serde 的三个救命属性

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

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

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

  • with 让你为第三方类型(chrono::DateTime、uuid::Uuid)定制序列化格式。
  • remote 让你给没有实现 Serde 的第三方库类型加 Serialize/Deserialize 支持——无需修改它们的源代码。
  • flatten 让你应对"嵌套 struct 但 JSON 扁平"的现实——比如 API 响应里公共字段(status、code)和特定字段混在一起。

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

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

本专栏基于 serde 1.0.228。

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

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

用户代码:

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

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

    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::serialize 和 iso_format::deserialize"。

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

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

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

// 字段位置生成
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> 把容器类型(含它的泛型参数)挂进 wrapper——这样容器的 where 子句能原样搬到 wrapper 上而不会出现"未使用的类型参数"。

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

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

Deserialize 侧类似(serde_derive/src/de.rs:664-708 的 wrap_deserialize_with,de/struct_.rs:254-258 调用它):

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

    // 临时 wrapper:value 存字段值,phantom/lifetime 只是类型标记
    #[doc(hidden)]
    struct __DeserializeWith<'de> {
        value: DateTime<Utc>,
        phantom: PhantomData<Event>,
        lifetime: PhantomData<&'de ()>,
    }

    #[automatically_derived]
    impl<'de> Deserialize<'de> for __DeserializeWith<'de> {
        fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
            Ok(__DeserializeWith {
                value: iso_format::deserialize(d)?,   // 调用用户函数
                phantom: PhantomData,
                lifetime: PhantomData,
            })
        }
    }

    __field0 = Some(match __map.next_value::<__DeserializeWith>() {
        Ok(__wrapper) => __wrapper.value,
        Err(__err) => return Err(__err),
    });
}

和序列化侧对称:wrapper 实现的是普通的 Deserialize(不是 DeserializeSeed),next_value::<__DeserializeWith>() 拿到 wrapper 后取出 .value 就是字段值。用户函数在 Deserialize::deserialize 里被调用。

serde_derive 的实现:Serialize 侧是 wrap_serialize_with(serde_derive/src/ser.rs:1207;wrap_serialize_field_with / wrap_serialize_variant_with 是它的两个入口,见 serde_derive/src/ser.rs:1173-1205),Deserialize 侧是 wrap_deserialize_with(serde_derive/src/de.rs:664)。生成临时 struct 的 quote! 模板是核心。

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

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

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)]——声明"这个 struct 是实现细节、不进文档"。要说清楚的是:这里的 #[doc(hidden)] 是冗余的防御,不是唯一把它藏起来的机制。quote!(&{ ... })(ser.rs:1241)把 __SerializeWith 生成在一个块表达式里——它是 fn serialize 函数体内的局部 item,rustdoc 本来就只收录从 crate 根可达的模块级 item,压根不会列它;何况整份 derive 产物还被 dummy::wrap_in_const 再包了一层 #[doc(hidden)] const _: () = { ... }(serde_derive/src/dummy.rs:15-30)。serde_derive 对所有生成的辅助 item 一律打这个标记,图的是生成代码风格一致、以及万一将来这段模板被挪到模块层级也不会漏。

② #[automatically_derived]——告诉工具链 "这个 impl 是宏自动生成的"。Rust Reference 对它的定义是 "It has no direct effect, but it may be used by tools and diagnostic lints to detect these automatically generated implementations"——它本身不改变语义,只是给工具一个标记。实际吃这个标记的地方之一是覆盖率插桩:rustc 从 rust-lang/rust#120185("coverage: Don't instrument #[automatically_derived] functions")起,不再给带此标记的 impl 里的函数插桩,派生实现不会以"未覆盖"的形式污染覆盖率报告。若干 clippy lint 也据此跳过生成代码。

这种"告诉工具链该忽略什么"的标注在 proc-macro 里很值钱——它们不改变程序行为,只改变工具看待这段代码的方式。serde_derive 的做法是"能标就标":辅助 struct 打 #[doc(hidden)]、生成的 impl 打 #[automatically_derived]、整份产物塞进匿名 const,三层叠加把生成代码从文档、lint、覆盖率报告里一起摘干净。

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

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

// 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 定义)——用户一看到"struct 定义这里有个奇怪的错误、找不到原因"。

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

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

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

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

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

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

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

// serde_derive/src/internals/attr.rs:830-845(变体级,在 Variant::from_ast 里;
// 字段级的一份同样的在 attr.rs:1124-1139,在 Field::from_ast 里)
} else if meta.path == WITH {
    // #[serde(with = "...")]
    if let Some(path) = parse_lit_into_expr_path(cx, WITH, &meta)? {
        let mut ser_path = path.clone();
        ser_path
            .path
            .segments
            .push(Ident::new("serialize", ser_path.span()).into());
        serialize_with.set(&meta.path, ser_path);
        let mut de_path = path;
        de_path
            .path
            .segments
            .push(Ident::new("deserialize", de_path.span()).into());
        deserialize_with.set(&meta.path, de_path);
    }
}

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

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

看 ser.rs:1216-1220:

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(serde_derive/src/bound.rs:354-385)干的事就是把用户 struct 的泛型参数(<T, U>)扩充成 <'__a, T: '__a, U: '__a>——在最前面加一个 lifetime,并给每个已有的类型参数和生命周期参数追加 : '__a 约束(引用 &'__a T 要求 T 至少活过 '__a)。where 子句靠 ..generics.clone() 原样带过来——用户 struct 有 where T: Clone、__SerializeWith 也有。

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

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

field_exprs.is_empty() 的分支——如果没有字段(比如标了 serialize_with 的 unit 变体,经 wrap_serialize_variant_with 进来)、不需要 lifetime、不走 bound 处理。这种按需注入的精细是 serde_derive 生成代码尽量简洁的体现——不该加的属性就不加。

16.3.2 attr.rs 的 WITH 属性展开:单行到两行的重写

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

为什么这个展开重要?

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

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

这种 "在尽量早的阶段把高层概念展开为低层原语" 的模式是编译器前端的经典设计——Rust 编译器本身把 for x in iter 展开成 loop { match iter.next() { ... } }、expr? 展开成对 Try::branch(expr) 的 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 的基础安全规则。

但用户有真实需求——第三方 crate 里没实现 Serde 的类型也得能序列化。Serde 的 remote 模式是绕过 orphan rule 的巧妙 workaround。下面沿用第 12 章的做法借 Duration 演示——要说明的是,serde 本身已经为 Duration 实现了 Serialize(serde_core/src/ser/impls.rs:685),反序列化一侧同样有内置实现(serde_core/src/de/impls.rs:2129),真实项目里不需要这样包;这里只是借一个人人熟悉的类型看 remote 的机制:

// 第一版:直接按字段名描述——这一版编译不过,见下文"坑"
#[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,而是挂在代理 struct 上的关联函数(serde_derive/src/ser.rs:29-44、serde_derive/src/de.rs:43-58;因为不能为外部类型 impl trait):

impl DurationDef {
    // 可见性跟随代理 struct 本身(#vis)
    fn serialize<S: Serializer>(__self: &Duration, serializer: S) -> Result<S::Ok, S::Error> {
        // 和普通 Serialize 生成相同逻辑,但用 __self 代替 self
        let mut state = serializer.serialize_struct("Duration", 2)?;
        state.serialize_field("secs", &__self.as_secs())?;
        state.serialize_field("nanos", &__self.subsec_nanos())?;
        state.end()
    }

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

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

#[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.rs 和 de.rs):

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

实际生成代码(对 DurationDef):

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

坑:std::time::Duration 的 secs / nanos 字段是私有的——第一版直接按字段名读,实测对 secs、nanos 各报一个 E0616(字段私有)。它的公共 API 是 as_secs() 和 subsec_nanos()。怎么把 DurationDef 的字段映射到 Duration 的 getter 方法?用 #[serde(getter = "fn")];反序列化方向同样不能按字段名构造私有字段,要再提供一个 From<DurationDef> for Duration:

use serde::{Deserialize, Serialize};
use std::time::Duration;

#[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 时,生成的 deserialize 先构造 DurationDef,再 Into 成 Duration
impl From<DurationDef> for Duration {
    fn from(def: DurationDef) -> Duration {
        Duration::new(def.secs, def.nanos)
    }
}

(这段加上前面的 MyConfig,在 serde 1.0.228 + serde_json 1.0.149 下实编实跑:MyConfig { timeout: Duration::new(3, 5) } 序列化为 {"timeout":{"secs":3,"nanos":5}},再反序列化回 3.000000005s。)

反序列化侧的 Into 转换见 serde_derive/src/de/struct_.rs:394-400:只要有字段带 getter,生成代码就把 DurationDef { .. } 包进 Into::<Duration>::into(..)。

序列化侧,生成代码用 getter 读取值(ser.rs get_member 的 (true, Some(getter)) 分支):

// __self: &Duration,getter 直接拿它当参数;constrain::<T> 把返回值钉死在字段声明的类型上
SerializeStruct::serialize_field(&mut state, "secs",
    _serde::__private228::ser::constrain::<u64>(&Duration::as_secs(__self)))?;
SerializeStruct::serialize_field(&mut state, "nanos",
    _serde::__private228::ser::constrain::<u32>(&Duration::subsec_nanos(__self)))?;

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

16.4.1 impl DurationDef——不是 impl Serialize for Duration

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

impl DurationDef {  // ← 关键:impl 的是代理 struct、不是远端类型
    fn serialize<__S>(__self: &Duration, __serializer: __S) -> ... { ... }
    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(__self, serializer) -> Result<...>)——inherent impl 没有 orphan rule 限制(DurationDef 是本地类型)、可以挂在 DurationDef 下面。

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

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

16.4.2 Parameters::self_var 的双模 ident

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

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_var 是 self 还是 __self。下游每一处 quote!(#self_var.field) 自动根据模式渲染成 self.field 或 __self.field。

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

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

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

16.4.3 quote_spanned! 和 this_type 的精确 remote 错误定位

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

let this_type = &params.this_type;

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

这和 self_var 是同一个思路:"当前类型是谁"这个分叉也在 Parameters 里一次算好,wrapper struct、PhantomData、函数签名里所有需要"当前类型"的位置都从 params.this_type 取,不用各处再判断 is_remote。remote 模式下真正被序列化的值是 Duration,wrapper 的类型标记也就跟着指向 Duration。

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

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

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

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

如果 check.rs 有 bug 漏了这个检查、unreachable! 会在 codegen 阶段 panic——panic 发生在宏展开期间,对用户表现为编译失败,bug 当场暴露、而不是 silent 生成错误代码。这是两层防御——先在 check 拦、拦不住就 unreachable 兜底。

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

16.5 flatten:最复杂的属性

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

#[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 对象里,反序列化时不知道哪些字段属于父级哪些属于子级。

序列化相对简单:

// 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:1003,FlatMapSerializer 的实现把 Serializer trait 的大部分方法变成错误分支:

// 节选,省略了 #[cfg] 和其余方法
pub struct FlatMapSerializer<'a, M: 'a>(pub &'a mut M);

impl<'a, M> FlatMapSerializer<'a, M>
where M: SerializeMap + 'a,
{
    fn bad_type(what: Unsupported) -> M::Error {
        ser::Error::custom(format_args!(
            "can only flatten structs and maps (got {})",
            what
        ))
    }
}

#[cfg_attr(not(no_diagnostic_namespace), diagnostic::do_not_recommend)]
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::Ok, Self::Error> {
        Err(Self::bad_type(Unsupported::Boolean))
    }
    fn serialize_i8(self, _: i8) -> Result<Self::Ok, Self::Error> {
        Err(Self::bad_type(Unsupported::Integer))
    }
    // ...(对标量类型都返回 bad_type)
    fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
        Err(Self::bad_type(Unsupported::Sequence))
    }
    // ...(serialize_tuple / serialize_tuple_struct 同理)
}

三层精心设计:

1. Impossible 关联类型封死不支持的组合——SerializeSeq/Tuple/TupleStruct 都设成 Impossible。trait 要求 serialize_seq 等方法的返回类型里有这个关联类型,而 Impossible 根本构造不出来,所以这三个方法只能走 Err 分支——"把序列 flatten 进 map"不是在类型层被拒绝,而是在运行时返回 bad_type(Unsupported::Sequence) 错误(实测 flatten 一个 Vec<u8> 字段,serde_json::to_string 返回 can only flatten structs and maps (got a sequence))。这正是第 3 章讲的 Impossible 用法,这里一口气用了 3 次。

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

3. #[cfg_attr(not(no_diagnostic_namespace), diagnostic::do_not_recommend)] 行(serde/src/private/ser.rs:1019)——这是 #[diagnostic] 命名空间下的诊断指令,告诉编译器当用户代码类型错误时不要推荐 FlatMapSerializer 这个 impl 作为修复建议。#[diagnostic] 命名空间 Rust 1.78 稳定(serde/build.rs:53-56 据此决定要不要打 no_diagnostic_namespace cfg),do_not_recommend 本身 1.85 才生效、更旧的编译器会忽略这个未知属性。FlatMapSerializer 是 Serde 内部的 plumbing 类型、用户误用#[serde(flatten)] 时看到编译器说"考虑 impl Serializer for FlatMapSerializer" 就完全没用。

支持的四种关联类型 SerializeMap/Struct/TupleVariant/StructVariant 各自是一个独立的 wrapper struct——每种"允许 flatten 的 Data Model 原语"配一个专用序列化器。这种 "关联类型栅栏" 的架构让每条允许的路径都有专用的状态对象、不允许的路径连状态对象都拿不到(只能返回错误)——是 Rust 类型系统表达能力在 Serde 内部的巅峰使用之一。

反序列化是真正的挑战:

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

解决方案:把不认识的 key 连同 value 缓存成 Content,已知字段照常直接反序列化(de/struct_.rs:225-282、337-362)。

// Response 的 visit_map(按 struct_.rs 生成代码精简)
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Response<T>, A::Error> {
    let mut status: Option<String> = None;
    let mut code: Option<u16> = None;
    // 未知 key 连同 value 缓存成 (Content, Content);被 flatten 字段消费掉的条目置 None
    let mut collect: Vec<Option<(Content, Content)>> = Vec::new();

    while let Some(key) = map.next_key::<__Field>()? {
        match key {
            __Field::status => status = Some(map.next_value()?),   // 已知字段:直接反序列化
            __Field::code => code = Some(map.next_value()?),
            // 有 flatten 时字段标识符多一个 __other(Content) 变体、把不认识的 key 原样存下来
            __Field::__other(name) => {
                collect.push(Some((name, map.next_value_seed(ContentVisitor::new())?)));
            }
        }
    }

    // 已知字段处理完后,flatten 字段从缓存里"捡走"属于它的 (key, value)
    let data: T = Deserialize::deserialize(FlatMapDeserializer(&mut collect, PhantomData))?;

    // 若容器还标了 deny_unknown_fields:此时 collect 里剩下的 Some(..) 才是真正的未知字段
    Ok(Response { status: status.unwrap(), code: code.unwrap(), data })
}

三步:

  1. 已知字段(status、code)不经过 Content、和普通 struct 一样直接 next_value
  2. 不认识的 key 和它的 value 缓存成 (Content, Content)——只有这部分输入被完整缓存
  3. flatten 字段用 FlatMapDeserializer 从缓存里消费:它把 Vec<Option<(Content, Content)>> 伪装成一个 map 源(serde/src/private/de.rs:3184)。flatten 成 struct 时,被认领的条目置成 None(flat_map_take_entry,serde/src/private/de.rs:3430-3447);flatten 成 map 时(FlatMapAccess)只借用、不置空

这就是 #[serde(flatten)] 比普通 struct 多出来的开销——flatten 字段的那部分输入要先缓存成 Content、再从 Content 二次反序列化,多一层分配和遍历。具体代价取决于 flatten 字段的深度和数量,性能敏感的场景应该用实测数据评估。

和 deny_unknown_fields 的关系:编译期不拦,但官方文档明确写着这个属性"不支持与 flatten 组合,无论标在外层 struct 还是被 flatten 的字段上"。源码里只做了一半:de/struct_.rs:347-361 的 collected_deny_unknown_fields 在 flatten 字段消费完缓存之后再检查 __collect 里剩下的 Some 条目——剩下的才报 unknown field。实测(serde 1.0.228 + serde_json 1.0.149)三种情况各不相同:外层标 deny_unknown_fields、flatten 一个 struct,多余的 zzz 会报 unknown field zzz,符合预期;标在被 flatten 的内层 struct 上则完全不生效,zzz 被静默接受;外层标 deny_unknown_fields、flatten 一个 HashMap,因为 map 只借用不置空,任何额外的键都会报 unknown field——flatten 出来的 map 等于白设。所以别把这两个属性搭在一起用。check.rs 对 flatten 只拒绝两种情况:tuple struct 和 newtype struct 上的 flatten(serde_derive/src/internals/check.rs:117-136;check_flatten 在 100-115 对 struct 字段和 enum 每个变体的字段逐个调用它)。

16.5.1 has_non_skipped_flatten 的编译期切换

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

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 由宏按它看得见的字段算出来,遇到 skip_serializing_if 时会变成形如 false as usize + 1 + if 条件 { 0 } else { 1 } 的运行时表达式,见 ser.rs:348-358)——但 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.2 FlatMapDeserializer 的两遍解析性能代价

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

Content 缓存的开销:

  • 堆分配——Content 是一个 enum Content<'de> { String(..), U64(u64), ... Map(Vec<(Content, Content)>) }——复杂输入会产生大量 Vec 分配
  • 拷贝——Content 有 Str(&'de str) / Bytes(&'de [u8]) 借用变体,但只有格式交出借用切片(visit_borrowed_str)时才用得上;有转义、或输入源是 io::Read 时就得 String / Vec<u8> 拷贝

flatten 的相对开销取决于被缓存的那部分输入占整体多大比例、嵌套多深。对 Web API 这类被网络 IO 主导的场景通常无感——但对批量反序列化(处理海量 JSON)或延迟敏感的 RPC 就要实测评估。

正确的使用场景:

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

如果非要在热路径用 flatten——先看嵌套是不是只有一层 newtype 包装(那 #[serde(transparent)] 零开销就够);否则手写 Deserialize(完全控制)。

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

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

// serde_core/src/private/content.rs:10(serde 1.0.220 起随 serde_core 拆分;serde 侧经 private::de 再导出)
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 章)里除 i128/u128 之外的全部标量;复合类型折叠成 Seq/Map/Newtype/Some,struct、tuple、enum 的"名字"在缓存时被丢掉。这让 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 值都是一个带 tag 的 enum、Seq/Map/Some/Newtype 又各自持有堆分配,输入嵌套多深就有多少层 Vec/Box。这就是 flatten / untagged 慢的根源——不只是遍历两次、还有这棵树的分配。

16.6 flatten + untagged enum 的组合绝招

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

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

直接这样写,公共字段 status 要在每个变体里重复一遍。更好的做法是把公共字段提到外层、用 flatten 接上:

#[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——第一个成功的胜出:

// 按 de/enum_untagged.rs:45-58 精简
fn deserialize<'de, D>(d: D) -> Result<Self, D::Error> {
    let content = ContentVisitor::new().deserialize(d)?;  // ← 缓存整个输入
    // 每个 variant 都用 ContentRefDeserializer 借用同一份 content,不 clone
    let d = ContentRefDeserializer::<D::Error>::new(&content);

    // 尝试 variant 1
    if let Ok(v) = /* Ok 变体的 visitor */.deserialize(d) {
        return Ok(Self::Ok(v));
    }
    // 尝试 variant 2
    if let Ok(v) = /* Err 变体的 visitor */.deserialize(d) {
        return Ok(Self::Err(v));
    }
    // 所有都失败
    Err(D::Error::custom("data did not match any variant of untagged enum Variant"))
}

性能问题叠加:

  • 缓存 Content——untagged 要把整个输入缓存成堆上的 Content 树(flatten 只缓存不认识的那部分字段)
  • 对每个 variant 尝试——N 个 variant 的 worst case 是 N 次完整解析

如果 enum 有 5 个 variant、输入是第 5 个——需要 4 次失败尝试 + 1 次成功——每次都是对同一份 Content 树的一遍遍历。

加上 flatten 的 combo——两种二次解析叠加——开销随 variant 数和嵌套深度一起涨。

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

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

16.7 属性之间的协作与冲突

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

  • #[serde(remote = "..", default)]:remote + default,处理远端类型的缺失字段
  • #[serde(flatten)] + rename_all:容器的 rename_all 只作用于它自己的字段;被 flatten 进来的字段名由内层类型自己的属性决定(FlatMapDeserializer 原样转交 key),两套规则互不影响
  • #[serde(with = "..")] + #[serde(default = "..")]:自定义序列化 + 默认值

哪些组合非法(都在 check.rs 里有对应的错误消息):

  • getter 只能出现在带 remote 的 struct 上,enum 里不允许(check_getter)
  • flatten 不能用在 tuple struct / newtype struct 的字段上(check_flatten_field)
  • 代理 struct 自己带泛型参数时,remote = "..." 路径不能再带具体泛型实参(check_remote_generic:"remove generic parameters from this path")
  • transparent 不能和 from / try_from / into 同用、也不能用在 enum 和 unit struct 上(check_transparent)
  • from 和 try_from 互斥(check_from_and_try_from)

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

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

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

// check.rs:8-19
pub fn check(cx: &Ctxt, cont: &mut Container, derive: Derive) {
    check_default_on_tuple(cx, cont);
    check_remote_generic(cx, cont);
    check_getter(cx, cont);
    check_flatten(cx, cont);
    check_identifier(cx, cont);
    check_variant_skip_attrs(cx, cont);
    check_internal_tag_field_name_conflict(cx, cont);
    check_adjacent_tag_conflict(cx, cont);
    check_transparent(cx, cont, derive);
    check_from_and_try_from(cx, cont);
}

// check.rs:117-136
fn check_flatten_field(cx: &Ctxt, style: Style, field: &Field) {
    if !field.attrs.flatten() {
        return;
    }
    match style {
        Style::Tuple => {
            cx.error_spanned_by(
                field.original,
                "#[serde(flatten)] cannot be used on tuple structs",
            );
        }
        Style::Newtype => {
            cx.error_spanned_by(
                field.original,
                "#[serde(flatten)] cannot be used on newtype structs",
            );
        }
        _ => {}
    }
}

每一条规则都有:

  1. 检查条件(if !field.attrs.flatten() { return; } + match style)
  2. 精确的错误消息(告诉用户哪两个属性冲突)
  3. 错误指向(通过 cx.error_spanned_by 指到用户写的具体属性)

这 477 行代码的价值不是"功能"、是"防护"——拦住用户写出语义上可能工作、但实际行为和期望不符的代码。比如在 tuple struct 的字段上写 flatten——生成代码没有字段名可以当 map key、错误会在离用户代码很远的地方冒出来;check.rs 直接一句 "#[serde(flatten)] cannot be used on tuple structs" 指着那个字段。又比如 getter 出现在非 remote struct 上——不拦的话会一路走到 get_member 的 unreachable!(§16.4.4)。check.rs 在编译期拦下这些陷阱、把错误指回用户写的属性。

这种 "宁可多报错、不让用户 silent bug" 的态度是高质量 Rust 库的共同特征——生态高质量的代价就是边界上的精确防御。

16.8 为什么这三个属性复杂

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

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

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

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

flatten 改变反序列化流程——未知字段要先缓存、再二次解析。整个 visit_map 逻辑多出一条缓存/消费路径。

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

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

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

serde_derive 约 9000 行代码(find serde_derive/src -name '*.rs' | xargs wc -l 在本专栏钉定的 commit fa7da4a 上实测 8969 行;v1.0.228 tag 为 8956 行,差异来自 tag 之后 31 个提交对 ser.rs、de.rs、bound.rs、attr.rs、ast.rs 等 9 个文件的小改)、分布在 28 个文件——每个模块职责明确:

模块 职责 代码量(wc -l 实测)
lib.rs 入口 + Serialize/Deserialize derive 宏导出 127 行
ser.rs Serialize impl 的代码生成 1369 行
de.rs + de/ 子目录(struct_ / tuple / enum_* / identifier 等 9 个文件) Deserialize impl 的代码生成 976 + 2383 行
serde_derive/src/internals/attr.rs 解析 #[serde(...)] 属性 1818 行
serde_derive/src/internals/case.rs rename_all 大小写转换 200 行
serde_derive/src/internals/check.rs 属性组合合法性 477 行
serde_derive/src/internals/ctxt.rs 错误累积(第 7 章 §7.12、第 10 章 §10.6 讲过) 67 行
bound.rs 自动 where 约束推导 425 行
dummy.rs 匿名 const 包装 31 行
pretend.rs dead_code 抑制(第 9 章讲过) 188 行

这个模块划分有几个 takeaway:

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

② attr.rs 1818 行——属性解析的细节极其繁琐。每一个 #[serde(xxx)] 都要做 syntax 合法性检查、位置检查(某些属性只能在字段上、某些只能在容器上)、meta 值格式验证。这 1800 多行是 serde 能支持 symbol.rs 里登记的 35 个属性名(容器 / 变体 / 字段三个层级各自取用)的基础。

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

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

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

16.9 实用模式:serde_with 生态

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

use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr, DurationSeconds};
use std::time::Duration;

#[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-macro proc-macro 加一层 #[serde_as] 预处理

具体对比:

// 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 方便得多。

实现原理——#[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 章"Runtime 可观测性"里的运行时指标有不少是 Duration(如 worker_total_busy_duration)——导出成 JSON 时要为它选格式:serde 自带的 {secs, nanos},或 serde_with 的 DurationSeconds。
  • 《MCP 协议设计与实现》专栏第 3 章讲的 JSON-RPC 消息,用 Rust 实现时 result 对象经常需要 #[serde(flatten)] 把公共字段(_meta)和方法特定字段合在同一层。
  • 《LangChain 设计与实现》专栏 里 Agent 的 tool schema 若用 Rust 实现,经常用 #[serde(rename)] + #[serde(with)] 组合——schema 字段名用 camelCase(API 约定),类型用 Rust 原生时间/UUID 等。

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

16.10.1 MCP 协议 result 对象里的 flatten 实例

第 14 章 §14.11 提过《MCP 协议设计与实现》专栏第 3 章的消息类型——如果用 Rust 实现一个 MCP server、result 对象的 flatten 用法几乎必然出现。JSON-RPC 2.0 规定 params 和 result 都是嵌套成员({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {...}}),所以请求信封本身不需要 flatten;需要 flatten 的是 result:MCP 的每种 result 都在方法特定字段旁边放一个可选的公共字段 _meta,两者在同一层:

{"tools": [...], "nextCursor": "...", "_meta": {...}}
#[derive(Serialize, Deserialize)]
struct McpResult<R> {
    #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
    meta: Option<Meta>,   // 所有 result 共有

    #[serde(flatten)]
    body: R,              // ← ListToolsResult { tools, next_cursor } 等方法特定字段被展平
}

如果不 flatten、就得写 {"body": {"tools": ...}} ——不符合 MCP schema。flatten 让 Rust 端的 McpResult<ListToolsResult> 天然产生符合 schema 的 JSON。

但这里有坑——§16.5.2 讲过 flatten 有缓存 + 二次解析的开销。MCP 的请求通常由用户交互驱动、频率不高、这点开销完全可以接受。但同样的模式到 RPC gateway 场景(每秒千级请求)——flatten 的开销就不可忽视。

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

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

LangChain 的 Python 代码建立在 pydantic 上,pydantic 有 @field_validator 和 @field_serializer 装饰器——对应 Rust 的 #[serde(with)]。如果用 Rust 重构 LangChain 的 Message 类型——一定会大量用 with:

#[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 实现:

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

#[serde(remote = "path::T")] 用代理类型上的关联函数替代 trait 实现绕过 orphan rule:

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

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

  1. 父级已知字段照常直接反序列化
  2. 不认识的 key/value 缓存成 (Content, Content)
  3. flatten 字段通过 FlatMapDeserializer 从缓存里消费属于它的条目

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

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

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

从日常读到的 Rust 项目看(经验观察、不是统计):

#[serde(with = "...")] 最普遍。最常见的 with 对象:

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

#[serde(flatten)] 次之。主要场景:

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

#[serde(remote = "...")] 最少用。因为:

  • 现代 Rust 类型几乎都有 serde feature(作为可选 feature)——不需要 remote 包装
  • 本章拿 std::time::Duration 做 remote 示例只是教学用(官方文档 serde.rs 的 remote 示例用的是虚构的 other_crate::Duration)——serde 自己一直带着 Duration / SystemTime 的 impl(serde_core/src/ser/impls.rs:685),直接 derive 就能用
  • 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 行组合验证、FlatMapSerializer 的 Impossible 关联类型禁用——每一个细节都在回答同一个问题:如何让一个 proc-macro 既功能强大、又用户友好、又错误信息精确、又性能可观?

serde_derive 的答案是把复杂度分层:

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

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

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

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

16.14 跨专栏呼应——serde_derive 的几个模式在其他专栏里的对应

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

与《Hyper 与 Tower:工业级 HTTP 栈》专栏第 14 章的呼应——serde 的 attr::Default 三态(None / Default / Path)和 hyper 的 Dur(Default / Configured 两个变体,各自携带 Option<Duration>,再配合 Time::Empty 区分是否装了 timer;hyper-1.9.0/src/common/time.rs:21-24)——都体现对 "用户显式设置 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 里的嵌套关系)

三者合起来、覆盖了绝大多数"我的类型不能直接用 derive、需要定制"场景。加上 serde_with 的预置抽象、剩下真正需要手写 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::Duration 或 std::net::IpAddr 写 remote 代理 struct,验证它能工作。
  3. flatten 性能实测:同一份结构,一次用 flatten,一次不用。用 criterion 对比反序列化 10MB JSON 的时间差异。
  4. 读 serde_with:这个 crate 是 with 属性的"标准库",读它的 README 和最常用的 10 个 serialize_as 类型,看它们如何实现。

延伸阅读

  • Serde "Custom serialization":自定义序列化的官方说明,with 指向的函数就按这里的签名来写。
  • serde_with:serde 生态最重要的辅助 crate,基于 with 提供几十种预置转换。
  • Serde "Deserialize for custom map type":为自定义 map 类型手写 Deserialize(Visitor + MapAccess)的完整示例。
  • 《Rust 编译器与运行时揭秘》专栏第 6 章 "单态化":理解 DurationSeconds<u64> vs DurationSeconds<f64> 如何编译成两份代码。
  • 《MCP 协议设计与实现》专栏第 3 章:看 flatten 在真实协议里的应用。