Skip to content

第 11 章 属性宏系统:#[serde(...)] 的解析机制

11.1 为什么属性系统如此庞大

Serde 的属性系统是它"好用"的秘密。用户可以写:

rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct User {
    #[serde(rename = "userId")]
    user_id: u64,

    #[serde(default)]
    display_name: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    avatar_url: Option<String>,

    #[serde(flatten)]
    settings: UserSettings,
}

短短十几行代码,声明了:字段命名风格转换、字段重命名、默认值、条件跳过、字段展平——五个独立的语义。每一个都对应序列化/反序列化逻辑的一处改动。

这些属性背后是 1818 行代码——serde_derive/src/internals/attr.rs。它是整个 serde_derive 最大的文件,比 Serialize 代码生成器还大。为什么?因为属性系统是用户接口——用户唯一和 serde_derive 直接交互的地方就是这些 #[serde(...)] 标注。它必须兼容、健壮、友好。

本章要做三件事:

  1. 把 serde 所有属性分类组织,形成完整参考。
  2. attr.rs 如何解析这些属性——Attr<T>BoolAttrVecAttr 三种工具模式。
  3. 理解属性的一致性检查(在 check.rs 里做)——哪些属性组合非法、如何报告。

读完本章,你对 #[serde(...)] 的每一个可能写法都能回答"它在 attr.rs 里是哪一块代码处理的"。

本章基于 serde 1.0.228。

11.2 Serde 属性的三个层级

Serde 属性按"附着的对象"分三个层级,每层控制不同范围的行为:

Container 属性attr::Container,约 18 种):控制整个类型的行为。例子:#[serde(rename_all = "camelCase")] 让所有字段自动转 camelCase。

Variant 属性attr::Variant,约 12 种):控制 enum 单个变体的行为。例子:#[serde(rename = "success")] Ok(T) 重命名变体。

Field 属性attr::Field,约 20 种):控制单个字段的行为。例子:#[serde(default)] 让字段缺失时用默认值。

三个层级有继承关系——Container 级的 rename_all 会作用于所有 Variant/Field,除非后者显式 override。

11.3 Container 属性的数据结构

先看最核心的 attr::Container(来自 serde_derive/src/internals/attr.rs:155):

rust
pub struct Container {
    name: MultiName,                          // rename / rename(serialize / deserialize)
    transparent: bool,                         // #[serde(transparent)]
    deny_unknown_fields: bool,                 // #[serde(deny_unknown_fields)]
    default: Default,                           // #[serde(default)] 或 #[serde(default = "...")]
    rename_all_rules: RenameAllRules,          // #[serde(rename_all = "camelCase")]
    rename_all_fields_rules: RenameAllRules,   // #[serde(rename_all_fields = "...")]
    ser_bound: Option<Vec<syn::WherePredicate>>, // #[serde(bound(serialize = "..."))]
    de_bound: Option<Vec<syn::WherePredicate>>,
    tag: TagType,                              // enum 的 tag 模式
    type_from: Option<syn::Type>,              // #[serde(from = "..")]
    type_try_from: Option<syn::Type>,          // #[serde(try_from = "..")]
    type_into: Option<syn::Type>,              // #[serde(into = "..")]
    remote: Option<syn::Path>,                 // #[serde(remote = "..")]
    identifier: Identifier,                    // #[serde(field_identifier)] 或 variant_identifier
    serde_path: Option<syn::Path>,             // #[serde(crate = "..")]
    is_packed: bool,                            // #[repr(packed)] 的副产物
    expecting: Option<String>,                  // #[serde(expecting = "..")]
    non_exhaustive: bool,                       // #[non_exhaustive] 的副产物
}

18 个字段涵盖所有可能的 container 级配置

注意几个细节:

  • 所有字段是私有的,通过 getter(pub fn name() -> &MultiNamepub fn tag() -> &TagType)访问。这是为了保护一致性——设置完后不允许外部修改。
  • name: MultiName 不是简单 String。因为 serde 支持 #[serde(rename(serialize = "foo", deserialize = "bar"))]——序列化和反序列化可以用不同名字。MultiName 同时存两个。
  • default: Default 是一个 enum,区分"没有 default"、"#[serde(default)](用 Default::default())"、"#[serde(default = "my_fn")]"三种。
  • tag: TagType 是 enum——External(默认)、InternalAdjacentNone(即 untagged)。这决定 enum 如何序列化,第 14 章重点内容。

11.4 解析器的三个工具:Attr / BoolAttr / VecAttr

attr.rs 开头的辅助结构(serde_derive/src/internals/attr.rs:24):

rust
pub(crate) struct Attr<'c, T> {
    cx: &'c Ctxt,
    name: Symbol,
    tokens: TokenStream,
    value: Option<T>,
}

impl<'c, T> Attr<'c, T> {
    fn none(cx: &'c Ctxt, name: Symbol) -> Self { ... }

    fn set<A: ToTokens>(&mut self, obj: A, value: T) {
        if self.value.is_some() {
            let msg = format!("duplicate serde attribute `{}`", self.name);
            self.cx.error_spanned_by(tokens, msg);
        } else {
            self.tokens = tokens;
            self.value = Some(value);
        }
    }

    pub(crate) fn get(self) -> Option<T> {
        self.value
    }
}

Attr<T> 是一个带重复检测的累积器

  • 多次 set 同一个属性会报"duplicate serde attribute"错误。
  • 它持有 Ctxt 引用,错误直接推到上下文的错误列表。
  • get() 消耗自己、返回最终值(Option,因为属性可能没出现)。

典型用法(来自 attr.rs 对 rename 的处理):

rust
let mut ser_name = Attr::none(cx, RENAME);

for attr in &item.attrs {
    if !attr.path().is_ident("serde") { continue; }
    attr.parse_nested_meta(|meta| {
        if meta.path == RENAME {
            let (s, de) = get_renames(cx, RENAME, &meta)?;
            ser_name.set_opt(&meta.path, s.map(|s| s.value()));
        }
        // ...
    })?;
}

// 最后
let name = ser_name.get();  // Option<String>

Attr<T> 是所有 "最多出现一次" 类型属性的标准模式。

BoolAttrAttr<()> 的包装:

rust
struct BoolAttr<'c>(Attr<'c, ()>);

impl<'c> BoolAttr<'c> {
    fn set_true<A: ToTokens>(&mut self, obj: A) {
        self.0.set(obj, ());
    }

    fn get(&self) -> bool {
        self.0.value.is_some()
    }
}

它用于 deny_unknown_fieldstransparentuntagged 等 boolean 属性。语义是"出现了就是 true,没出现就是 false"。

VecAttr<T> 是"可能出现多次的属性":

rust
pub(crate) struct VecAttr<'c, T> {
    cx: &'c Ctxt,
    name: Symbol,
    first_dup_tokens: TokenStream,
    values: Vec<T>,
}

用于 #[serde(alias = "x")]#[serde(alias = "y")] 这种——用户可能写多个 alias,需要全部保留。

11.4.1 被忽略的三个细节:set_if_none / get_with_tokens / at_most_one

上面列了 Attr / BoolAttr / VecAttr 三个容器、但实际 attr.rs:24-131 里还有几个真实存在但容易被漏看的方法——它们是 "属性系统的边缘工程":

1. Attr::set_if_none(line 59-63)——只在还没设置时才设

rust
fn set_if_none(&mut self, value: T) {
    if self.value.is_none() {
        self.value = Some(value);
    }
}

它和 set 的区别很关键:set 对重复赋值报错set_if_none 对重复赋值静默跳过。用途:默认值填充。比如解析时如果用户没显式写 rename、容器级的 rename_all 规则可以通过 set_if_none 给每个字段注入一个默认改名结果——不覆盖用户已显式设置的 rename。这区分了"用户明确指定" vs "编译器补默认" 两种来源、避免默认值把用户选择覆盖掉。

2. Attr::get_with_tokens(line 69-74)——返回值附带 span

rust
fn get_with_tokens(self) -> Option<(TokenStream, T)> {
    match self.value {
        Some(v) => Some((self.tokens, v)),
        None => None,
    }
}

get() 丢掉 span 信息只返回值、get_with_tokens() 保留当初 set 时的 tokens。用途:延迟错误报告。一个属性当初 set 时是合法的、但在后续一致性检查(11.8 节)里发现和另一个属性冲突——这时错误应该指向原始 set 的 span 位置(即用户写这个属性的地方)、不是检查发生的地方。get_with_tokens 让 11.8 节的一致性检查能用精确的 span 报错。

3. VecAttr::at_most_one(line 117-126)——"宽松收集 + 严格使用"的消费者

rust
fn at_most_one(mut self) -> Option<T> {
    if self.values.len() > 1 {
        let dup_token = self.first_dup_tokens;
        let msg = format!("duplicate serde attribute `{}`", self.name);
        self.cx.error_spanned_by(dup_token, msg);
        None
    } else {
        self.values.pop()
    }
}

同一个 VecAttr<T> 容器——解析阶段允许重复、消费阶段决定是否允许——同样的 insert 收集、在 get()at_most_one() 两种消费者间选择:

  • get() -> Vec<T>:调用方接受 "0 个或多个"(alias、rename_deserialize 等)
  • at_most_one() -> Option<T>:调用方要求 "最多 1 个",> 1 个就报错

这种 "容器中立、消费者决定策略" 的设计让同一数据结构支持两种语义。例如 serialize_withdeserialize_with 在某些语境下允许重复(不同 serializer 变体)、在另一语境下必须唯一——用 VecAttr 收集、在消费点选正确的出口函数即可。

first_dup_tokens(line 96, 111-112)的特殊记录——当 insert 第二次时(len() == 1 时)记下第二个写入的 tokens(而非第一个)。at_most_one 报错时用这个 span 指人:"你这里多写了一个、原版在别处"——这比指向第一个(用户以为自己只写了这一次)更友好。这种 span 选择的细节体现了 dtolnay 代码里处处可见的"为用户错误信息"工程思维。

11.4.2 unraw 和 raw identifier 处理

attr.rs:133 还有一个小工具函数经常被漏看:

rust
fn unraw(ident: &Ident) -> Ident {
    Ident::new(ident.to_string().trim_start_matches("r#"), ident.span())
}

它处理 raw identifierr#typer#match 等)——Rust 允许用户把保留关键字作为标识符使用、前缀 r# 转义。如果用户写 struct Response { r#type: String, status: u32 }、Serde 序列化出来的 JSON 应该是 {"type": "...", "status": 0} 而不是 {"r#type": "..."}——r# 只是语法转义、不是真正的名字一部分

unraw 在每处需要"把 ident 转成 string 作为字段名"的地方被调用、trim_start_matches("r#")r# 前缀吃掉。这个六行函数是 Serde 支持"用户字段名是 Rust 保留字"场景的基础——没它的话 {"r#type": ...} 这种奇怪的 JSON 会泄漏出来。

这种"不起眼的规整函数在角落默默工作"是 Serde 作为基础设施级库的可靠性来源——每一个用户可能遇到的语法细节都有人替你想过。

这三个工具覆盖了 serde 属性的所有出现形态(0-1 次、布尔、多次)。attr.rs 里几百行代码围绕它们打转。

11.5 解析主流程:Container::from_ast

Container::from_ast 的骨架(serde_derive/src/internals/attr.rs:237,简化):

rust
impl Container {
    pub fn from_ast(cx: &Ctxt, item: &syn::DeriveInput) -> Self {
        // 1. 初始化所有属性为"未设置"状态
        let mut ser_name = Attr::none(cx, RENAME);
        let mut de_name = Attr::none(cx, RENAME);
        let mut transparent = BoolAttr::none(cx, TRANSPARENT);
        let mut deny_unknown_fields = BoolAttr::none(cx, DENY_UNKNOWN_FIELDS);
        let mut default = Attr::none(cx, DEFAULT);
        let mut rename_all_ser_rule = Attr::none(cx, RENAME_ALL);
        let mut rename_all_de_rule = Attr::none(cx, RENAME_ALL);
        let mut internal_tag = Attr::none(cx, TAG);
        let mut content = Attr::none(cx, CONTENT);
        let mut untagged = BoolAttr::none(cx, UNTAGGED);
        // ... 其他 15+ 个属性初始化

        // 2. 遍历类型上的所有属性
        for attr in &item.attrs {
            if !attr.path().is_ident("serde") { continue; }

            // 3. 对每个 #[serde(...)] 内部的每一项 meta 做匹配
            if let Err(err) = attr.parse_nested_meta(|meta| {
                if meta.path == RENAME {
                    // #[serde(rename = "...")]
                    let (ser, de) = get_renames(cx, RENAME, &meta)?;
                    ser_name.set_opt(&meta.path, ser.map(|s| s.value()));
                    de_name.set_opt(&meta.path, de.map(|s| s.value()));
                } else if meta.path == RENAME_ALL {
                    // #[serde(rename_all = "camelCase")]
                    let one_name = meta.input.peek(Token![=]);
                    // ... 解析 rename_all
                } else if meta.path == TRANSPARENT {
                    // #[serde(transparent)]
                    transparent.set_true(&meta.path);
                } else if meta.path == DENY_UNKNOWN_FIELDS {
                    deny_unknown_fields.set_true(&meta.path);
                } else if meta.path == DEFAULT {
                    // #[serde(default)] 或 #[serde(default = "...")]
                    if meta.input.peek(Token![=]) {
                        // 带自定义函数
                        let f: syn::ExprPath = meta.value()?.parse()?;
                        default.set(&meta.path, Default::Path(f));
                    } else {
                        default.set(&meta.path, Default::Default);
                    }
                } else if meta.path == TAG {
                    // #[serde(tag = "...")]
                    if let Some(s) = get_lit_str(cx, TAG, &meta)? {
                        internal_tag.set(&meta.path, s.value());
                    }
                }
                // ... 其他属性的处理
                else {
                    let path = meta.path.to_token_stream().to_string();
                    return Err(meta.error(format_args!(
                        "unknown serde container attribute `{}`", path
                    )));
                }
                Ok(())
            }) {
                cx.syn_error(err);
            }
        }

        // 4. 组装成 Container 结构
        Container {
            name: MultiName::from_attrs(Name::from(&item.ident), ser_name, de_name),
            transparent: transparent.get(),
            deny_unknown_fields: deny_unknown_fields.get(),
            default: default.get().unwrap_or(Default::None),
            tag: decide_tag(internal_tag, content, untagged),
            // ...
        }
    }
}

四步:

  1. 初始化所有属性变量为 none(表示"还没被设置")。
  2. 遍历类型上的所有属性(一个类型可能有多个 #[serde(...)])。
  3. 每个 #[serde(...)] 内部遍历每个 meta 项(一个 #[serde(a=1, b=2, c)] 里有三个 meta)。每个 meta 用 if meta.path == XXX 分派到对应处理。
  4. 组装最终 Container

整个 attr.rs 的模式就是这样——18 种 Container 属性、12 种 Variant 属性、20 种 Field 属性,每种都有一个 if meta.path == ... 分支。重复但清晰。

Symbol 模块serde_derive/src/internals/symbol.rs)定义了所有属性名的常量:

rust
// serde_derive/src/internals/symbol.rs
pub struct Symbol(&'static str);

pub const RENAME: Symbol = Symbol("rename");
pub const DEFAULT: Symbol = Symbol("default");
pub const FLATTEN: Symbol = Symbol("flatten");
pub const TAG: Symbol = Symbol("tag");
// ... 共 35 个属性名常量

实测:symbol.rs 文件里只有 35 个常量(不是 70)——按字母序:

ALIAS / BORROW / BOUND / CONTENT / CRATE / DEFAULT / DENY_UNKNOWN_FIELDS / DESERIALIZE / DESERIALIZE_WITH / EXPECTING / FIELD_IDENTIFIER / FLATTEN / FROM / GETTER / INTO / NON_EXHAUSTIVE / OTHER / REMOTE / RENAME / RENAME_ALL / RENAME_ALL_FIELDS / REPR / SERDE / SERIALIZE / SERIALIZE_WITH / SKIP / SKIP_DESERIALIZING / SKIP_SERIALIZING / SKIP_SERIALIZING_IF / TAG / TRANSPARENT / TRY_FROM / UNTAGGED / VARIANT_IDENTIFIER / WITH

35 个常量为啥能撑起 18+12+20 = 50 种属性?因为同一个常量在 Container/Variant/Field 三层都被复用。比如 RENAME 出现在三层、BOUND 出现在两层、SKIP_SERIALIZING 出现在两层。复用是属性表面"种类多"但底层"原子少"的关键

rust
impl PartialEq<Symbol> for syn::Path {
    fn eq(&self, word: &Symbol) -> bool {
        self.is_ident(word.0)
    }
}

注意 impl PartialEq<Symbol> for syn::Path——给 syn::Path 加了 "和 Symbol 比较" 的能力。这让代码写出来很自然:

rust
if meta.path == RENAME { ... }

而不是 if meta.path.is_ident("rename") { ... }。小细节,但让 attr.rs 的 1800 行代码可读性显著提升。symbol.rs 同时给 Ident&IdentPath&Path 各 impl 了一次 PartialEq——四份实现保证调用点不需要 & / deref 相关的 borrow 体操。

11.6 Rename 规则的实现

#[serde(rename_all = "camelCase")]user_id 自动转成 userId。这种命名转换在 serde_derive/src/internals/case.rs 的 200 行代码里实现。

RenameRule 的 enum 定义:

rust
// serde_derive/src/internals/case.rs
pub enum RenameRule {
    None,
    LowerCase,        // "lowercase"
    UpperCase,        // "UPPERCASE"
    PascalCase,       // "PascalCase"
    CamelCase,        // "camelCase"
    SnakeCase,        // "snake_case"
    ScreamingSnakeCase, // "SCREAMING_SNAKE_CASE"
    KebabCase,        // "kebab-case"
    ScreamingKebabCase, // "SCREAMING-KEBAB-CASE"
}

impl RenameRule {
    pub fn apply_to_variant(self, variant: &str) -> String {
        // variant 通常是 PascalCase(Rust 约定)
        // 根据规则转换
        use self::RenameRule::*;
        match self {
            None => variant.to_owned(),
            LowerCase => variant.to_ascii_lowercase(),
            UpperCase => variant.to_ascii_uppercase(),
            PascalCase => variant.to_owned(),
            CamelCase => {
                variant[..1].to_ascii_lowercase() + &variant[1..]
            },
            SnakeCase => {
                let mut out = String::new();
                for (i, c) in variant.char_indices() {
                    if i > 0 && c.is_uppercase() {
                        out.push('_');
                    }
                    out.push_ascii_lowercase(c);
                }
                out
            },
            // ...
        }
    }

    pub fn apply_to_field(self, field: &str) -> String {
        // field 通常是 snake_case(Rust 约定)
        // 转换规则略有不同
        ...
    }
}

两个入口函数

  • apply_to_variant:变体从 PascalCase 起始转换。
  • apply_to_field:字段从 snake_case 起始转换。

为什么分两个? 因为 Rust 约定里变体是 PascalCase、字段是 snake_case。从不同起始状态转换需要不同逻辑。如果只有一个通用函数,每种转换都要先识别"当前是哪种风格",反而更复杂。serde 通过语义上下文(知道是 field 还是 variant)选对函数。

这一段代码是本书第一次看到 serde 的"工程细致"的典型体现——对真实 Rust 代码习惯的深入理解,反映在 API 设计里。

11.6.1 RenameRule 实测:9 种风格、源码组合实现

case.rs:9-33 的 enum 实测有 9 个 variant——

rust
pub enum RenameRule {
    None, LowerCase, UpperCase, PascalCase, CamelCase,
    SnakeCase, ScreamingSnakeCase, KebabCase, ScreamingKebabCase,
}

算法实现的精妙——ScreamingSnake / Kebab / ScreamingKebab 全部基于 SnakeCase 二次组合case.rs:74-78):

rust
ScreamingSnakeCase => SnakeCase.apply_to_variant(variant).to_ascii_uppercase(),
KebabCase => SnakeCase.apply_to_variant(variant).replace('_', "-"),
ScreamingKebabCase => ScreamingSnakeCase.apply_to_variant(variant).replace('_', "-"),

只有 SnakeCase 是真正"算法实现"——遍历字符、检测大写、插入 _——其他三种(Screaming / Kebab / ScreamingKebab)都是 SnakeCase 的字符串后处理。9 种 rule、3 段核心算法 + 6 段 transform——这就是 dtolnay 代码"用最少代码覆盖最多场景"的典型。

11.6.2 apply_to_variant vs apply_to_field 的非对称性

apply_to_variant 假设输入是 PascalCase(UserId)、apply_to_field 假设输入是 snake_case(user_id)——起点不同、转换路径不同

举一个反直觉例子——#[serde(rename_all = "snake_case")] 应用在不同对象的结果

目标apply_to_variantapply_to_field
UserId (variant)snake_caseuser_idn/a
user_id (field)snake_casen/auser_id(不变)
userId (field)snake_casen/auser_id(按"驼峰边界"插 _

apply_to_field 的 SnakeCase 分支注意点——当输入已经是 snake_case 时不动(早返回 field.to_owned());只有当输入是 camel/Pascal 时才转。这避免了 user_id 被错误转成 u_s_e_r_id 的低级 bug。

11.6.3 from_str 的设计:硬编码字符串数组

case.rs:25-33RENAME_RULES 是一个 (&str, RenameRule) 数组:

rust
const RENAME_RULES: &[(&str, RenameRule)] = &[
    ("lowercase", LowerCase),
    ("UPPERCASE", UpperCase),
    ("PascalCase", PascalCase),
    ("camelCase", CamelCase),
    ("snake_case", SnakeCase),
    ("SCREAMING_SNAKE_CASE", ScreamingSnakeCase),
    ("kebab-case", KebabCase),
    ("SCREAMING-KEBAB-CASE", ScreamingKebabCase),
];

用户字符串和 enum 之间用线性搜索匹配——for (name, rule) in RENAME_RULES { if rename_all_str == *name { return Ok(*rule); } }

为啥不用 HashMap?—— 8 项的线性搜索比 HashMap 还快(cache-friendly、无 hash 计算)。dtolnay 永远选最简单的够用方案——这是 serde 整体能保持高性能的微观体现。

(&str, ...) 数组的字符串值同时是 user-facing API——用户在 #[serde(rename_all = "camelCase")] 里写的字符串必须精确匹配这 8 个之一——大小写敏感、连字符 vs 下划线敏感。这种"硬编码集合 = 公开 API 契约"的模式让用户输错时报错精确("unknown rename_all rule 'camelcase' — did you mean 'camelCase'?")。

11.7 Field 属性:20 多种选项

Field 属性是 serde 最丰富的一层。看 attr::Field 的部分字段(来自 attr.rs):

rust
pub struct Field {
    name: MultiName,                         // rename / rename(ser/de)
    aliases: BTreeSet<Name>,                 // alias
    skip_serializing: bool,                  // skip_serializing
    skip_deserializing: bool,                // skip_deserializing
    skip_serializing_if: Option<syn::ExprPath>, // skip_serializing_if = "fn"
    default: Default,                        // default 或 default = "fn"
    serialize_with: Option<syn::ExprPath>,   // serialize_with = "fn"
    deserialize_with: Option<syn::ExprPath>, // deserialize_with = "fn"
    ser_bound: Option<Vec<syn::WherePredicate>>, // bound(serialize = "..")
    de_bound: Option<Vec<syn::WherePredicate>>,
    borrow: Option<BorrowAttribute>,         // borrow = "'a" 或 borrow
    getter: Option<syn::ExprPath>,           // getter = "fn"
    flatten: bool,                           // flatten
    transparent: bool,                       // transparent
    // ...
}

每个属性对应一种代码生成行为。比如:

  • rename = "x":序列化时字段名用 "x"
  • skip_serializing_if = "Option::is_none":如果表达式求值为 true,这个字段不写入。
  • with = "module::path":用 module::path::serializemodule::path::deserialize 替代默认的字段序列化。
  • flatten:把字段的 struct 内容"展平"到父级 struct。
  • borrow:反序列化时借用 'de 生命周期的输入数据。

这些属性交互产生了 serde 的"高级用法"——第 16 章会专门讨论 withflattenremotegetter 等属性的内部机理。

11.7.1 Field 实测:13 个字段不是 20 个

§11.1 提到 Field 属性"约 20 种"——attr.rs:978Field 结构体实际只有 13 个字段。这两个数字的差距在哪?

rust
pub struct Field {
    name: MultiName,                              // ① rename / rename(ser/de) / alias 三个属性折叠到一处
    skip_serializing: bool,                       // ②
    skip_deserializing: bool,                     // ③
    skip_serializing_if: Option<syn::ExprPath>,   // ④
    default: Default,                              // ⑤ default + default = "fn" 折叠
    serialize_with: Option<syn::ExprPath>,        // ⑥
    deserialize_with: Option<syn::ExprPath>,      // ⑦
    ser_bound: Option<Vec<syn::WherePredicate>>,  // ⑧
    de_bound: Option<Vec<syn::WherePredicate>>,   // ⑨
    borrowed_lifetimes: BTreeSet<syn::Lifetime>,  // ⑩
    getter: Option<syn::ExprPath>,                // ⑪
    flatten: bool,                                // ⑫
    transparent: bool,                            // ⑬
}

13 字段 vs 20 属性——差异来自三处折叠:

  • name: MultiName——同时承载 renamerename(serialize = "x")rename(deserialize = "y")alias = "z" 四种用户写法、合并为一个数据结构
  • default: Default——一个 enum、三种状态(None / Default / Path(fn))覆盖 default / default = "fn" 两种属性写法
  • with = "module" 不存为独立字段——而是在解析时展开serialize_with = "module::serialize" + deserialize_with = "module::deserialize" 两个字段值

这个折叠Field 结构体更紧凑——但代价是用户写错时错误信息要"反向追溯"。比如 with 属性的错误消息可能只指向 serialize_with 字段——这在 ergonomics 上是个小妥协。

11.7.2 Variant 实测:11 个字段对应 12+ 种属性

attr.rs:728Variant

rust
pub struct Variant {
    name: MultiName,                              // rename + alias
    rename_all_rules: RenameAllRules,             // rename_all
    ser_bound: Option<Vec<syn::WherePredicate>>,  // bound(serialize)
    de_bound: Option<Vec<syn::WherePredicate>>,   // bound(deserialize)
    skip_deserializing: bool,                     // skip_deserializing
    skip_serializing: bool,                       // skip_serializing
    other: bool,                                  // other(用于 untagged enum 的"兜底"variant)
    serialize_with: Option<syn::ExprPath>,
    deserialize_with: Option<syn::ExprPath>,
    borrow: Option<BorrowAttribute>,              // borrow / borrow = "'a, 'b"
    untagged: bool,                                // untagged(仅当 enum tag mode 也是 untagged 时该 variant 才允许)
}

11 字段——每一个都对应一个真实属性。值得注意——

  • other: bool——一个被 99% 用户忽略的属性。配合 #[serde(untagged)] 用——#[serde(other)] 标记的 variant 是"我是兜底、其他 variant 都不匹配时落到我"——本质是 "fallback variant"。
  • untagged: bool——和 Container 级的 tag 配合——只有当容器是 untagged 模式时、variant 级的 untagged 才有意义(覆盖部分 variant 不参与 tag 推断)。

11.7.3 BorrowAttribute 数据结构

attr.rs:741 定义了一个特殊辅助结构:

rust
struct BorrowAttribute {
    path: syn::Path,
    lifetimes: Option<BTreeSet<syn::Lifetime>>,
}

两种写法、统一存储

  • #[serde(borrow)] —— lifetimes = None(自动推导所有出现在字段类型里的生命周期)
  • #[serde(borrow = "'a + 'b")] —— lifetimes = Some({'a, 'b})(显式指定)

为什么用 BTreeSet 而不是 Vec——保证:(1) 去重——用户重复写同一个 'a 不会出问题;(2) 顺序稳定——derive 生成代码每次都一致、便于 incremental compilation 缓存命中。

Field 上的对应字段是 borrowed_lifetimes: BTreeSet<syn::Lifetime>(直接 set、没有 path 包装)——因为 Field 不需要"用户没写显式 lifetimes 时存 Path 来回报错"——它直接计算出 effective lifetimes 集合存进去。两层对同一个 attribute 用了不同精度的数据结构——上层(Variant)保留原始信息便于报错、下层(Field)只保留 derived 结果便于代码生成。

11.7.4 borrow 自动推导的算法

当用户写 #[serde(borrow)] 不指定 lifetimes 时——serde 必须自动推导"这个字段的类型用了哪些生命周期"。算法在 attr.rs:1700+ 附近的 borrowable_lifetimes(&Field) -> BTreeSet<Lifetime>

  • 遍历 field 的 syn::Type
  • 用一个 Visit 实现遍历整个类型 AST
  • 收集所有 Lifetime 节点
  • 排除 'static(不能"借用 static 生命周期"——本身就是永久)

典型例子——

  • &'a str{'a}
  • Cow<'a, str>{'a}
  • (&'a str, &'b u32){'a, 'b}
  • Vec<&'a str>{'a}
  • &'static str{}(static 被排除)

这个推导是本书第 15 章讨论的"零拷贝反序列化"的入口——#[serde(borrow)] 触发推导、生成的 Deserialize impl 带 'de: 'a 约束、最终让 &'de str 字段能直接借用输入 buffer。

11.8 属性的一致性检查

解析完属性后,要做一致性检查——某些属性组合非法,要及时报错。这在 serde_derive/src/internals/check.rs(477 行)里完成。

典型检查

rust
// 伪代码
pub fn check(cx: &Ctxt, cont: &Container, derive: Derive) {
    // 1. tag + untagged 不能共存
    if cont.attrs.tag() != &TagType::External
        && cont.attrs.untagged() {
        cx.error_spanned_by(
            cont.original,
            "#[serde(tag = ..)] cannot be combined with #[serde(untagged)]",
        );
    }

    // 2. transparent 类型必须只有一个非 skip 字段
    if cont.attrs.transparent() {
        let fields_count = non_skipped_fields(&cont.data);
        if fields_count != 1 {
            cx.error_spanned_by(
                cont.original,
                "#[serde(transparent)] requires exactly one serializable field",
            );
        }
    }

    // 3. deny_unknown_fields 不能和 flatten 共存
    for field in fields(&cont.data) {
        if field.attrs.flatten() && cont.attrs.deny_unknown_fields() {
            cx.error_spanned_by(
                field.original,
                "#[serde(flatten)] cannot be combined with #[serde(deny_unknown_fields)]",
            );
        }
    }

    // ... 几十条其他规则
}

每一条规则都对应一个真实的 bug 或歧义场景。比如 flatten + deny_unknown_fields:flatten 意味着"吸纳未知字段到被展平的 struct",deny_unknown_fields 意味着"拒绝未知字段"——两者根本冲突。静默会导致行为不确定;check.rs 明确报错。

这 477 行代码是 Serde 用户体验的关键。用户如果偶然写了冲突的属性组合,不会得到"运行时行为奇怪"的 bug,而是编译期明确错误,指明哪两个属性冲突

11.8.1 check.rs 实测:11 个 check 函数、约 40 处 error_spanned_by

不是"几十条规则"——check.rs 实际有 11 个独立 check 函数、内部累计调用 cx.error_spanned_by约 40 次。每次代表一种独立的合法性约束:

check 函数行号检测什么
check_default_on_tuple27tuple struct 上的 #[serde(default)] 用法限制
check_remote_generic66#[serde(remote = "Path<T>")] 不能带泛型参数
check_getter78getter 必须配合 remote、且不能用在 newtype struct
check_flatten100flatten 字段不能和某些容器属性共存
check_flatten_field117flatten 字段不能用在 tuple struct
check_identifier144field_identifier / variant_identifier 的强约束(必须是 enum、variant 不能有字段、等)
check_variant_skip_attrs226enum variant 的 skip 属性组合限制(如 untagged + skip 冲突)
check_internal_tag_field_name_conflict300internally-tagged enum 时、tag 字段名不能撞 variant 内的字段名
check_adjacent_tag_conflict352adjacently-tagged 时 tag 和 content 字符串不能相同
check_transparent370#[serde(transparent)] 必须只有一个非 skip 字段(且自动 forward 实现)
check_from_and_try_from470fromtry_from 互斥、不能同时设

11 个函数 × 平均 4 处错误点 ≈ 40 次 error_spanned_by。每一次都对应一个生产环境真实出现过的用户错误模式——所以才被加进 check.rs。

check_transparent 是唯一签名带 &mut Container——它会在确认合法时把 transparent 标志真正写进 Container(而不是仅在解析时)——这是为了等所有字段处理完、知道最终非 skip 字段数量后才能定论。check 同时承担"验证 + 后处理"双重职责。

11.8.2 TagType enum 的四种形态

attr.rs:178TagType 是 enum 序列化策略的核心数据结构、对应四种 JSON 形态:

rust
pub enum TagType {
    External,                                    // 默认:{"variant1": {...}}
    Internal { tag: String },                    // {"type": "variant1", ...}
    Adjacent { tag: String, content: String },   // {"t": "variant1", "c": {...}}
    None,                                        // {...}(即 untagged)
}

每种形态对应不同的属性组合——

  • External:用户什么都不写、默认
  • Internal:用户写 #[serde(tag = "type")]
  • Adjacent:用户写 #[serde(tag = "t", content = "c")]
  • None:用户写 #[serde(untagged)]

check_internal_tag_field_name_conflict (line 300) 专门检测 Internal { tag: "type" } 时、enum 各 variant 内部不能有叫 type 的字段——否则 JSON 序列化会撞名、反序列化无法区分"这是 tag 还是 fieldName"。

check_adjacent_tag_conflict (line 352) 专门检测 Adjacent { tag: "t", content: "c" } 时、t != c 必须成立——否则同一个 key 既要承载 variant name 又要承载内容、JSON 结构无意义。

这两条 check 是 enum 反序列化在边界条件下的关键守门员——没它们用户会得到运行时怪异行为而非编译错误。本书第 14 章专门讲 enum 标签策略——会再次回到这个 enum。

11.9 属性继承:rename_all 的层级传播

Container 级的 rename_all 会影响所有字段。这个"继承"是怎么实现的?看 ast.rs 里的处理(已经在第 10 章简略提过,这里深入):

rust
// 简化版 ast.rs
impl<'a> Container<'a> {
    pub fn from_ast(cx: &Ctxt, item: &'a syn::DeriveInput, derive: Derive, ...) -> Option<Container<'a>> {
        let attrs = attr::Container::from_ast(cx, item);
        let mut data = match &item.data {
            // ... 先解析 struct / enum 结构
        };

        // 关键:应用 rename_all 到字段
        match &mut data {
            Data::Enum(variants) => {
                for variant in variants {
                    // 变体名字按 container 的 rename_all 规则转换
                    variant.attrs.rename_by_rules(attrs.rename_all_rules());

                    for field in &mut variant.fields {
                        // 字段名字按 variant 自己的 rename_all 或 container 的 rename_all_fields 规则
                        field.attrs.rename_by_rules(
                            variant.attrs.rename_all_rules()
                                .or(attrs.rename_all_fields_rules())
                        );
                    }
                }
            }
            Data::Struct(_, fields) => {
                for field in fields {
                    field.attrs.rename_by_rules(attrs.rename_all_rules());
                }
            }
        }
        // ...
    }
}

三层继承

  1. Container 的 rename_all_rules 影响所有变体名(如果是 enum)和所有字段名(如果是 struct)。
  2. Container 的 rename_all_fields_rules 影响 enum 各变体内部的字段名
  3. Variant 的 rename_all_rules 影响该变体内部的字段名,优先级高于 Container 的 rename_all_fields。

rename_by_rules 方法真实代码(Variant 版本来自 serde_derive/src/internals/attr.rs:925,Field 版本在 :1272):

rust
// attr.rs:925  — Variant::rename_by_rules
pub fn rename_by_rules(&mut self, rules: RenameAllRules) {
    if !self.name.serialize_renamed {
        self.name.serialize.value =
            rules.serialize.apply_to_variant(&self.name.serialize.value);
    }
    if !self.name.deserialize_renamed {
        self.name.deserialize.value = rules
            .deserialize
            .apply_to_variant(&self.name.deserialize.value);
    }
    self.name
        .deserialize_aliases
        // ... 对 aliases 也应用规则
}

三个关键细节

  • serialize_renamed / deserialize_renamed 是两个独立 bool,记录"用户是否显式 rename 过"——不是我原稿写的 has_renamed_ser() 方法。
  • 应用 apply_to_variant 而不是 apply_to_field——因为这是 Variant 的方法,变体名按变体规则转(PascalCase→其它)。Field 的版本(:1272)调 apply_to_field
  • aliases 也参与转换——用户写 #[serde(alias = "x")] 时,x 也会被 rename_all 处理。

关键:用户显式的 #[serde(rename = "x")] 优先级高于 rename_all——用户 override 了默认转换,不应该再被自动改。

这种"用户显式 > 继承 > 默认"的三级优先是配置系统的典型设计。Serde 做的精细度在这里体现得很充分。

11.9.5 三个支撑模块:ast.rs / name.rs / ctxt.rs

attr.rs 1831 行不是孤立工作——internals/ 目录下还有三个短小关键的支撑模块:

文件职责
ast.rs218syn::DeriveInput 转 serde 自己的 Container/Data/Variant/Field
name.rs113MultiName 数据结构(serialize / deserialize 双名 + alias 集合)
ctxt.rs68Ctxt 错误累积器(强制 check 的 panic-on-drop 设计)

这三个加 attr.rs 共 2230 行 = serde_derive "input 处理"全部代码

11.9.5.1 ast.rs:从 syn AST 到 serde AST 的"双层"设计

ast.rs:10 定义的 Container<'a>attr.rs:155Container两个不同结构——

rust
// ast.rs:10
pub struct Container<'a> {
    pub ident: syn::Ident,
    pub attrs: attr::Container,  // ← 注意:attr.rs 的 Container 在这里作为字段
    pub data: Data<'a>,           // Struct(Style, Vec<Field>) | Enum(Vec<Variant>)
    pub generics: &'a syn::Generics,
    pub original: &'a syn::DeriveInput,  // 原 AST 备份用于错误 span
}

ast::Container 持有 attr::Container——两层 container 各司其职

  • attr::Container——纯属性配置(rename、tag、deny_unknown_fields 等 18 个字段)
  • ast::Container——完整 derive 输入(attrs + data + generics + 原 AST 引用)

为啥要分两层——职责分离attr.rs 只关心"用户写了什么属性"、ast.rs 关心"完整的代码生成上下文"。ser.rs / de.rs(代码生成器)拿 ast::Container 即可——无需重新解析属性。

11.9.5.2 ctxt.rs:panic-on-drop 强制错误检查

ctxt.rs:62-69 的 Drop 实现值得单独写一节

rust
impl Drop for Ctxt {
    fn drop(&mut self) {
        if !thread::panicking() && self.errors.borrow().is_some() {
            panic!("forgot to check for errors");
        }
    }
}

强制约束——Ctxt 在 drop 时如果还没被 check() 消费——直接 paniccheck() 内部把 errorsSome(Vec) 设成 None(line 49 注释明确说),让 drop 时通过检测。

为啥要这么严——防 silent error。serde_derive 解析属性时所有错误都累积到 Ctxt、不立即抛——意图是"一次报告所有错误"——但如果 caller 忘了调 check 就 drop、所有错误就丢了——derive 会"成功"但生成有问题的代码。

!thread::panicking() 守卫——如果当前已经在 panicking(比如因为别的错),不再雪上加霜 panic 一次(避免双 panic 导致 abort)。

这种"用 Drop 强制 API 调用顺序"的模式——是 Rust 生态里的典型"linear type 模拟"——线性类型用 Drop 来强制资源被正确消费。同源的设计:tokio 的 JoinHandle 不 await 时不 panic(默认不强制)、但 sqlx 的 Transaction 不 commit 时回滚——Drop 的语义可以承载丰富的 API 契约

11.9.5.3 name.rs:MultiName 的双轨字段名

name.rs:113MultiName——核心数据结构:

rust
// 简化
pub struct MultiName {
    pub serialize: Name,
    pub serialize_renamed: bool,           // 用户是否显式 rename serialize
    pub deserialize: Name,
    pub deserialize_renamed: bool,         // 用户是否显式 rename deserialize
    pub deserialize_aliases: BTreeSet<Name>,  // alias 集合
}

serialize_renamed / deserialize_renamed 两个 bool——记录"用户是否显式rename 过"。这是为了让 rename_by_rules(§11.9)能区分"用户已选名"和"默认推断名"——用户显式 rename 永远优先

为什么不用 Option<Name> 表达"未设置"——因为 Name 字段总有值(默认从 ident 推断、即使没 rename 也是 ident.to_string())——bool 比 Option 更准确表达"是否被用户主动改过"

这种"区分默认值用户值"的设计在配置系统里很重要——比如 git config 区分 git config --defaultgit config --global、Kubernetes 区分 spec 和 status——任何"层级覆盖系统"都需要这个 bool

11.10 和 parse_nested_meta 的关系

第 7 章讲过 attr.parse_nested_meta(|meta| ...) 是 syn 2.x 的便利 API。serde_derive 大量用它——整个 attr.rs 的解析逻辑都建立在这个 API 之上。

为什么 serde 不自己写 parser? 历史上 serde 曾经有自己的手写 parser(syn 1.x 时代)。syn 2.x 引入 parse_nested_meta 后,serde 迁移到它——因为:

  1. 错误信息更好(syn 的默认错误带精准 span)。
  2. 兼容性更好(支持 =(...) 两种风格)。
  3. 代码少得多(手写 parser 曾经有 500+ 行)。

这是一个工程"借东风"的典范——serde 作者 dtolnay 也是 syn 的作者,他在 syn 里加的 API 首先服务自己的 serde。社区反过来受益。

11.11 cargo expand 实例:属性如何影响生成代码

看一个真实例子对比不同属性组合的生成效果。

例子 A:无特殊属性

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

生成(精简):

rust
_serde::Serializer::serialize_struct(__serializer, "User", 2)?;
SerializeStruct::serialize_field(&mut __state, "id", &self.id)?;
SerializeStruct::serialize_field(&mut __state, "name", &self.name)?;

例子 B:rename_all

rust
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct User { user_id: u64, display_name: String }

生成:

rust
SerializeStruct::serialize_field(&mut __state, "userId", &self.user_id)?;
SerializeStruct::serialize_field(&mut __state, "displayName", &self.display_name)?;

变化:字段 key 从 "user_id"/"display_name" 变成 camelCase。

例子 C:skip_serializing_if

rust
#[derive(Serialize)]
struct User {
    id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    email: Option<String>,
}

生成:

rust
let mut __state_size = 1;  // id 一定序列化
if !Option::is_none(&self.email) { __state_size += 1; }

let mut __state = _serde::Serializer::serialize_struct(__serializer, "User", __state_size)?;
SerializeStruct::serialize_field(&mut __state, "id", &self.id)?;
if !Option::is_none(&self.email) {
    SerializeStruct::serialize_field(&mut __state, "email", &self.email)?;
} else {
    SerializeStruct::skip_field(&mut __state, "email")?;
}
__state.end()

变化:先计算实际字段数(运行时),然后对 email 字段用 if 包裹。如果为 None 则 skip_field(而不是不处理)——因为某些格式需要知道"有这个字段但被跳过"。

这些生成代码的差异就是 attr.rs 里对应属性处理的直接产物。每一处属性都会让代码生成器走不同路径。

11.12 和丛书其他书的关联

属性系统是一种通用模式。其他 Rust 库用同样的思路做用户配置:

  • Tokio 的 #[tokio::main(flavor = "current_thread")]:属性宏,不同 flavor 生成不同 runtime 启动代码。丛书《Tokio 源码深度解析》第 20 章讨论过它。
  • clap 的 #[arg(short, long, default_value_t = 42)]:每个字段属性控制命令行参数解析行为。结构和 serde Field 属性高度相似。
  • diesel 的 #[diesel(table_name = "users")]:ORM 的字段映射,结构类似。

丛书《MCP 协议设计与实现》第 3 章里讨论过 JSON-RPC 消息的 Rust 定义,大量使用 #[serde(tag = "method", content = "params")] 这种 adjacently-tagged 模式——那是本章讲的属性系统在协议实现里的真实应用。

11.13 本章小结

Serde 的属性系统是 1818 行代码撑起的用户接口。它的核心组织方式:

  1. 三个层级:Container / Variant / Field,层级间有继承关系。
  2. 三个工具Attr<T>(最多 1 次)、BoolAttr(0/1)、VecAttr<T>(多次)。
  3. 解析流程:遍历属性 → parse_nested_meta 分派 → 组装结构体。
  4. 一致性检查check.rs 里 477 行规则覆盖所有非法组合。
  5. 命名转换case.rs 200 行实现 9 种 rename 风格。
  6. 继承传播:Container 的 rename_all 传到 Variant 再传到 Field,用户显式 override 优先。

生产级过程宏的"属性系统"就该这么做。如果你写自己的 derive 宏需要属性,照着这个模式组织——一致、可维护、错误信息清晰。

下一章进入 Serialize 代码生成——用 Container 和解析出的属性信息生成 impl Serialize for T 的完整代码。你会看到第 3 章讲的 Serializer 协议如何在这里落地成真实 quote! 模板。

动手实验

  1. 阅读 attr.rs 第 237-547 行(Container::from_ast 完整体)。这约 300 行代码用最直白的方式展示了 serde 如何解析 18 种 container 属性。
  2. 给 serde 加一个新属性(fork 自己试):比如 #[serde(log_on_serialize)],让生成代码在序列化时打印 log。步骤:(a) 在 symbol.rs 加常量;(b) 在 attr::Container 加字段;(c) 在 from_ast 里解析;(d) 在 ser.rs 里用这个字段决定是否生成 log 代码。不完全实现也行,理解流程就好。
  3. 读 check.rs:随便挑 3 条 check 规则,理解它们为什么存在——写两个会触发这些规则的示例。
  4. 对比 rename_all 转换效果#[serde(rename_all = "camelCase")] 对字段名 user_id 和变体名 UserId 的转换结果是不同的——它们分别调用 apply_to_fieldapply_to_variant。自己推导一下转换结果,再用 cargo expand 验证。

延伸阅读

  • Serde Attributes 官方文档:完整的属性清单和用法说明。本章是"实现视角",这份文档是"用户视角"——对照看效果最好。
  • serde_derive/src/internals/attr.rs 完整源码:1818 行,读一遍对 serde 的所有细节都有体感。
  • syn::meta::ParseNestedMeta 文档:serde 解析属性用的核心 syn API。
  • 丛书《Tokio 源码深度解析》第 20 章:看 #[tokio::main] 的属性处理作对比。
  • 丛书《MCP 协议设计与实现》第 3 章:看属性如何应用在真实 JSON-RPC 协议定义里。

基于 VitePress 构建