Serde 元编程

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

作者 杨艺韬 · 10,309 字 · 发布于 · 更新于

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

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

#[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 的属性按层级分类组织(逐条清单以官方 Attributes 文档为准,见文末延伸阅读)。
  2. 看 attr.rs 如何解析这些属性——Attr<T>、BoolAttr、VecAttr 三种工具模式。
  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,13 种):控制 enum 单个变体的行为。例子:#[serde(rename = "success")] Ok(T) 重命名变体。

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

三个层级有继承关系——Container 级的 rename_all 会作用于 enum 的所有变体名或 struct 的所有字段名,除非后者显式 override(enum 变体内部字段的改名走变体自己的 rename_all 或容器的 rename_all_fields,见 §11.9)。

11.3 Container 属性的数据结构

先看最核心的 attr::Container(来自 serde_derive/src/internals/attr.rs:155,行尾注释为本专栏所加):

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() -> &MultiName、pub 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(默认)、Internal、Adjacent、None(即 untagged)。这决定 enum 如何序列化,第 14 章重点内容。

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

看 attr.rs 开头的辅助结构(serde_derive/src/internals/attr.rs:24,省略了 set_opt、set_if_none、get_with_tokens,后两个见 §11.4.1):

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) {
        let tokens = obj.into_token_stream();

        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 的处理):

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

for attr in &item.attrs {
    if attr.path() != SERDE { continue; }
    if let Err(err) = attr.parse_nested_meta(|meta| {
        if meta.path == RENAME {
            let (ser, de) = get_renames(cx, RENAME, &meta)?;
            ser_name.set_opt(&meta.path, ser.as_ref().map(Name::from));
        }
        // ...
        Ok(())
    }) {
        cx.syn_error(err);
    }
}

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

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

BoolAttr 是 Attr<()> 的包装:

// serde_derive/src/internals/attr.rs:77
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_fields、transparent、untagged 等 boolean 属性。语义是"出现了就是 true,没出现就是 false"。

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

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 三个容器、但实际 serde_derive/src/internals/attr.rs:24-131 里还有几个真实存在但容易被漏看的方法——它们是 "属性系统的边缘工程":

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

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

它和 set 的区别很关键:set 对重复赋值报错、set_if_none 对重复赋值静默跳过。用途:默认值填充——attr.rs 里它只用在三类场合(共 5 个调用点),语义都是"已经有值就不覆盖":(1) Variant / Field 上的 rename(deserialize = "a", deserialize = "b") 允许写多个反序列化名,第一个通过 de_name.set_if_none 成为主名、全部进 alias 集合(line 782 / 1081);(2) 字段标了 skip_deserializing 但自己和容器都没写 default 时,default.set_if_none(Default::Default) 让它落到 Default::default()(line 1192);(3) #[serde(borrow)] 打在 Cow<str> / Cow<[u8]> 字段上时,deserialize_with.set_if_none(...) 注入内置的借用实现 _serde::__private228::de::borrow_cow_str / borrow_cow_bytes——用户自己写了 deserialize_with 就不覆盖(line 1222 / 1239)。这区分了"用户明确指定" vs "编译器补默认" 两种来源、避免默认值把用户选择覆盖掉。(容器级 rename_all 的传播不走 set_if_none,而是 ast.rs 里的 rename_by_rules,见 §11.9。)

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

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 时是合法的、但组装 Container 时发现和另一个属性冲突——这时错误应该指向原始 set 的 span 位置(即用户写这个属性的地方)、不是检查发生的地方。attr.rs 里它只在两个函数里被调用:decide_tag(line 622:untagged / tag / content 三者的组合合法性,比如 "enum cannot be both untagged and internally tagged" 会同时指向 untagged 和 tag 两处 span)和 decide_identifier(line 683:field_identifier 与 variant_identifier 不能同时设)。

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

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 个就报错

这种 "容器中立、消费者决定策略" 的设计让同一数据结构支持两种语义。真实用例是 rename(serialize = ..., deserialize = ...):get_ser_and_de(line 1341)把 serialize / deserialize 两侧各收进一个 VecAttr;Container 级的 get_renames 对两侧都调 at_most_one(各最多一个名字),而 Variant / Field 级的 get_multiple_renames 对 serialize 侧调 at_most_one、对 deserialize 侧调 get()——所以 #[serde(rename(deserialize = "a", deserialize = "b"))] 在字段上合法(多出来的成为 alias)、在容器上报 duplicate。bound(serialize = ..., deserialize = ...) 同理走 get_where_predicates,两侧都 at_most_one。

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

11.4.2 unraw 和 raw identifier 处理

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

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

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

unraw 在把类型名、变体名、字段名的 ident 转成序列化名字的三处被调用(attr.rs:520 / 896 / 1039)、trim_start_matches("r#") 把 r# 前缀吃掉。这个三行函数是 Serde 支持"用户字段名是 Rust 保留字"场景的基础——没它的话 {"r#type": ...} 这种奇怪的 JSON 会泄漏出来。

这种"不起眼的规整函数在角落默默工作",处理的正是用户容易踩到的语法细节。

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

11.5 解析主流程:Container::from_ast

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

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);
        // ... 其他 13 个属性初始化(bound / from / try_from / into / remote / crate / expecting 等)

        // 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.as_ref().map(Name::from));
                    de_name.set_opt(&meta.path, de.as_ref().map(Name::from));
                } 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 = "...")]
                    // (真实代码还会检查只允许用在有字段的 struct 上)
                    if meta.input.peek(Token![=]) {
                        // 带自定义函数
                        if let Some(path) = parse_lit_into_expr_path(cx, DEFAULT, &meta)? {
                            default.set(&meta.path, Default::Path(path));
                        }
                    } 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 结构(中间还扫一遍 #[repr(...)] 得到 is_packed)
        Container {
            name: MultiName::from_attrs(Name::from(&unraw(&item.ident)), ser_name, de_name, None),
            transparent: transparent.get(),
            deny_unknown_fields: deny_unknown_fields.get(),
            default: default.get().unwrap_or(Default::None),
            tag: decide_tag(cx, item, untagged, internal_tag, content),
            identifier: decide_identifier(cx, item, field_identifier, variant_identifier),
            // ...
        }
    }
}

四步:

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

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

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

// serde_derive/src/internals/symbol.rs:5
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 个常量——按字母序:

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 个常量为啥能撑起三层加起来四五十种属性写法?因为同一个常量在 Container/Variant/Field 三层都被复用。比如 RENAME、BOUND 都出现在三层,SKIP_SERIALIZING 出现在两层。复用是属性表面"种类多"但底层"原子少"的关键。

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 比较" 的能力。这让代码写出来很自然:

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

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

11.6 Rename 规则的实现

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

看 RenameRule 的 enum 定义:

// 简化自 serde_derive/src/internals/case.rs:9-109(中文注释为本专栏所加)
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 | PascalCase => variant.to_owned(),
            LowerCase => variant.to_ascii_lowercase(),
            UpperCase => variant.to_ascii_uppercase(),
            CamelCase => variant[..1].to_ascii_lowercase() + &variant[1..],
            SnakeCase => {
                let mut snake = String::new();
                for (i, ch) in variant.char_indices() {
                    if i > 0 && ch.is_uppercase() {
                        snake.push('_');
                    }
                    snake.push(ch.to_ascii_lowercase());
                }
                snake
            }
            // ...
        }
    }

    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)选对函数。

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

serde_derive/src/internals/case.rs:9-31 的 enum 实测有 9 个 variant(None 加 8 种风格)——

pub enum RenameRule {
    /// Don't apply a default rename rule.
    None,
    /// Rename direct children to "lowercase" style.
    LowerCase,
    /// Rename direct children to "UPPERCASE" style.
    UpperCase,
    /// Rename direct children to "PascalCase" style, as typically used for
    /// enum variants.
    PascalCase,
    /// Rename direct children to "camelCase" style.
    CamelCase,
    /// Rename direct children to "snake_case" style, as commonly used for
    /// fields.
    SnakeCase,
    /// Rename direct children to "SCREAMING_SNAKE_CASE" style, as commonly
    /// used for constants.
    ScreamingSnakeCase,
    /// Rename direct children to "kebab-case" style.
    KebabCase,
    /// Rename direct children to "SCREAMING-KEBAB-CASE" style.
    ScreamingKebabCase,
}

算法实现的精妙——ScreamingSnake / Kebab / ScreamingKebab 全部基于 SnakeCase 二次组合(serde_derive/src/internals/case.rs:73-77):

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

apply_to_variant 里只有 SnakeCase 是真正"算法实现"——遍历字符、检测大写、插入 _——其他三种(Screaming / Kebab / ScreamingKebab)都是 SnakeCase 的字符串后处理。apply_to_field 对称:只有 PascalCase 是循环(按 _ 切词、首字母大写),CamelCase 是 PascalCase 再把首字母小写,Screaming / Kebab 则是 to_ascii_uppercase / replace('_', "-")。8 种风格、两个方向各一段核心循环、其余全是 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_variant apply_to_field
UserId (variant) snake_case user_id n/a
user_id (field) snake_case n/a user_id(不变)
userId (field,不合 Rust 惯例) snake_case n/a userId(原样返回,不会插 _)

apply_to_field 的 SnakeCase 分支注意点——它和 None / LowerCase 共用一个分支、直接返回 field.to_owned():serde 假定字段名已经是 snake_case,不做任何驼峰边界检测。所以对一个不合惯例的 userId 字段写 rename_all = "snake_case" 什么也不会发生(case.rs 里 rename_fields 测试断言的正是 SnakeCase.apply_to_field(original) == original)。反过来 LowerCase.apply_to_field("very_tasty") 也是原样返回、下划线不会被去掉——这些都是"起点假设"带来的边界。

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

serde_derive/src/internals/case.rs:33-42 的 RENAME_RULES 是一个 (&str, RenameRule) 数组:

static 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 项的线性搜索对编译期一次性的属性解析完全够用、还省掉建表和一次 hash 计算。

(&str, ...) 数组的字符串值同时是 user-facing API——用户在 #[serde(rename_all = "camelCase")] 里写的字符串必须精确匹配这 8 个之一——大小写敏感、连字符 vs 下划线敏感。这种"硬编码集合 = 公开 API 契约"的模式让用户输错时报错精确——ParseError 的 Display 实现会打出 unknown rename rule `rename_all = "camelcase"`, expected one of "lowercase", "UPPERCASE", ...,把这 8 个合法值全部列出来。

11.7 Field 属性:14 种选项

Field 属性是 serde 最丰富的一层。看 attr::Field 的全部字段(来自 attr.rs,行尾注释为本专栏所加):

pub struct Field {
    name: MultiName,                         // rename / rename(ser/de) / alias(alias 集合在 MultiName 里)
    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>>,
    borrowed_lifetimes: BTreeSet<syn::Lifetime>, // borrow = "'a" 或 borrow 推导出的生命周期集合
    getter: Option<syn::ExprPath>,           // getter = "fn"
    flatten: bool,                           // flatten
    transparent: bool,                       // transparent(由 check.rs 的 mark_transparent 写入)
}

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

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

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

11.7.1 Field 实测:13 个字段对应 14 种属性

§11.2 提到 Field 属性有 14 种用户可写的属性(serde.rs 官方文档的 field attributes 页列出的正是这 14 个名字)——但 serde_derive/src/internals/attr.rs:978 的 Field 结构体只有 13 个字段,而且其中 transparent 并不对应任何字段属性。这两个数字的差距在哪?

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,                            // ⑬
}

14 种属性 vs 12 个属性字段(外加 check.rs 写入的 transparent)——两边不是一一对应,而是有合并也有拆分:

  • name: MultiName——同时承载 rename、rename(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" 两个字段值;skip 同理展开为 skip_serializing + skip_deserializing
  • bound 反过来被拆成 ser_bound / de_bound 两个字段

这个折叠让 Field 结构体更紧凑——但代价是用户写错时错误信息要"反向追溯"。比如同一字段上同时写 with = "m" 和 serialize_with = "f",报的是 duplicate serde attribute `serialize_with` ——错误里的名字是展开后的 serialize_with,不是用户写的 with(serde 1.0.228 实测)。这在 ergonomics 上是个小妥协。

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

serde_derive/src/internals/attr.rs:728 的 Variant(行尾注释为本专栏所加):

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(见下文)
}

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

  • other: bool——一个很少有人用的属性。#[serde(other)] 标记的 unit variant 是"tag 不匹配任何已知 variant 时落到我"——本质是 "fallback variant",用于 internally / adjacently tagged enum 以及 field_identifier enum(check_identifier 要求它必须是 unit variant、且必须是最后一个 variant;untagged enum 上不允许用)。它和 #[serde(untagged)] 无关。
  • untagged: bool——variant 级的 #[serde(untagged)],让一个否则带 tag 的 enum 混入几个 untagged variant:ast.rs 的 enum_from_ast 要求所有 untagged variant 必须排在 enum 末尾,de/enum_.rs 生成的代码先按 tag 模式尝试前面的 variant、失败后再逐个试 untagged variant。

11.7.3 BorrowAttribute 数据结构

serde_derive/src/internals/attr.rs:742 定义了一个特殊辅助结构:

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) 查重——parse_lit_into_lifetimes 往集合里 insert 失败时报 duplicate borrowed lifetime `'a` (attr.rs:1553-1558,borrow = "'a + 'a" 实测报此错);(2) 顺序稳定——迭代顺序确定,derive 每次生成的代码都一致。

Field 上的对应字段是 borrowed_lifetimes: BTreeSet<syn::Lifetime>(直接 set、没有 path 包装)——它存的是算好的 effective lifetimes 集合。两层对同一个 attribute 用了不同精度的数据结构,原因是求值要延后:Variant 解析时只知道用户写了什么(lifetimes 可能是 None),而且 borrow 只允许用在 newtype variant 上(attr.rs:870-878);要等 Field::from_ast 处理那个唯一字段、结合字段类型调用 borrowable_lifetimes 时才能算出实际集合(attr.rs:1045-1058),path 则留作这时报错和查重的 span。下层(Field)只保留 derived 结果便于代码生成。

11.7.4 borrow 自动推导的算法

当用户写 #[serde(borrow)] 不指定 lifetimes 时——serde 必须自动推导"这个字段的类型用了哪些生命周期"。算法在 serde_derive/src/internals/attr.rs:1716 的 borrowable_lifetimes(cx, name, field) -> Result<BTreeSet<Lifetime>, ()>:

  • 调 collect_lifetimes(&field.ty, &mut out)(line 1732)——一个手写的、按 syn::Type 各变体分别递归的 match(不是 syn::visit;BareFn、TraitObject、ImplTrait 等不往下走,宏类型则扫描其 token),把类型里出现的 Lifetime 收进 BTreeSet
  • 集合为空就报错 field `x` has no lifetimes to borrow
  • 'static 不在这里排除——它照常收集;到了 de.rs 的 borrowed_lifetimes(cont) 汇总时,只要任何字段借了 'static,整个 impl 就用 BorrowedLifetimes::Static(生成 Deserialize<'static> 而不是 Deserialize<'de>,因为 'de: 'static 是多余的)

典型例子——

  • &'a str → {'a}
  • Cow<'a, str> → {'a}
  • (&'a str, &'b u32) → {'a, 'b}
  • Vec<&'a str> → {'a}
  • &'static str → {'static}(随后由 de.rs 折叠成 Deserialize<'static>)

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

11.8 属性的一致性检查

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

典型检查:

// serde_derive/src/internals/check.rs:8(真实入口;各 check_* 的实现有删节)
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);
}

// 1. flatten 不能用在 tuple / newtype struct 上
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"),
        _ => {}
    }
}

// 2. transparent 类型必须恰好有一个"没被 skip 的"字段(PhantomData 不算)
//    (Serialize 看 skip_serializing,Deserialize 看 skip_deserializing + default)
fn check_transparent(cx: &Ctxt, cont: &mut Container, derive: Derive) {
    // ... 不能和 from / try_from / into 共存,不能用在 enum / unit struct 上
    // ... 找到唯一的 transparent_field,多于一个就报
    //     "#[serde(transparent)] requires struct to have at most one transparent field"
    // ... 找到了就 transparent_field.attrs.mark_transparent()
}

// 3. from 和 try_from 互斥
fn check_from_and_try_from(cx: &Ctxt, cont: &mut Container) {
    if cont.attrs.type_from().is_some() && cont.attrs.type_try_from().is_some() {
        cx.error_spanned_by(cont.original,
            "#[serde(from = \"...\")] and #[serde(try_from = \"...\")] conflict with each other");
    }
}

每一条规则都对应一个真实的 bug 或歧义场景。比如 flatten 打在 tuple struct 的字段上:flatten 的语义是"把子结构的 key 摊进父级 map",而 tuple struct 序列化成的是没有 key 的序列——两者根本对不上。静默会导致行为不确定;check.rs 明确报错。注意 check.rs 开头的注释:这里只放"需要同时看多个 attrs 对象"的交叉检查,同一个 attrs 对象内部的冲突(比如容器上的 tag + untagged)在 attr.rs 解析阶段的 decide_tag 里就报掉了;而 flatten + deny_unknown_fields 这种组合 check.rs 里没有对应检查——因为它根本不需要拦:1.0.228 的 derive 会为这个组合额外生成一段 collected_deny_unknown_fields(serde_derive/src/de/struct_.rs:347-363),在 flatten 字段吃完缓存之后检查 __collect 里剩下的条目并报 unknown field,test_suite/tests/test_annotations.rs:2129-2165 就是这个组合的回归测试(详见第 16 章 §16.5)。

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

11.8.1 check.rs 实测:10 个 check 函数、29 处 error_spanned_by

check.rs 的 check() 实际调用 10 个 check 函数(外加 check_flatten 的 helper check_flatten_field)、内部累计调用 cx.error_spanned_by 共 29 次(grep -c error_spanned_by)。每次代表一种独立的合法性约束:

check 函数 行号 检测什么
check_default_on_tuple 27 tuple struct 上的 #[serde(default)] 用法限制
check_remote_generic 66 本地类型带泛型参数时,#[serde(remote = "Path<T>")] 的路径不能再带泛型参数
check_getter 78 getter 不能用在 enum 里;用在 struct 里时必须配合 remote
check_flatten 100 遍历 struct / 各 variant 的字段,逐个交给 check_flatten_field
check_flatten_field 117 flatten 字段不能用在 tuple struct / newtype struct 上
check_identifier 144 field_identifier / variant_identifier 的强约束(必须是 enum、variant 不能有字段、等)
check_variant_skip_attrs 226 variant 上的 serialize_with / deserialize_with 不能与该 variant 或其字段上的 skip_serializing(_if) / skip_deserializing 同时出现
check_internal_tag_field_name_conflict 300 internally-tagged enum 时、tag 字段名不能撞 variant 内的字段名
check_adjacent_tag_conflict 352 adjacently-tagged 时 tag 和 content 字符串不能相同
check_transparent 370 #[serde(transparent)] 不能配 from / try_from / into、不能用在 enum / unit struct、必须恰好一个非 skip 字段
check_from_and_try_from 470 from 和 try_from 互斥、不能同时设

11 个函数、29 次 error_spanned_by。每一次都对应一种需要跨多个 attrs 对象才能发现的非法组合。

check_transparent 和 check_from_and_try_from 的签名带 &mut Container——前者会在确认合法时调用 transparent_field.attrs.mark_transparent(),把 transparent 标志写到那个唯一字段的 attr::Field 上(Field::transparent 这个字段只有这一处写入点,解析阶段从不设置它)——这是为了等所有字段处理完、知道哪个字段是"非 skip 的那一个"后才能定论。check 同时承担"验证 + 后处理"双重职责。

11.8.2 TagType enum 的四种形态

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

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 会影响所有变体名(enum)或字段名(struct)。这个"继承"是怎么实现的?看 ast.rs 里的处理(已经在第 10 章简略提过,这里深入):

// 简化版 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):

// 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
        .insert(self.name.deserialize.clone());
}

三个关键细节:

  • serialize_renamed / deserialize_renamed 是两个独立 bool,记录"用户是否显式 rename 过"(MultiName 的字段,见 §11.9.4)。
  • 应用 apply_to_variant 而不是 apply_to_field——因为这是 Variant 的方法,变体名按变体规则转(PascalCase→其它)。Field 的版本(:1272)调 apply_to_field。
  • aliases 不参与转换、但会补上主名——用户写的 #[serde(alias = "x")] 原样保留;转换完成后把(可能已改名的)deserialize 主名也 insert 进 alias 集合,这样反序列化侧的字段识别只需要看一个集合。

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

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

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

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

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

这三个加 attr.rs 共 2223 行,是 serde_derive 处理输入的主体(internals/ 下另有 check.rs 477 行、case.rs 200 行、symbol.rs 71 行等)。

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

serde_derive/src/internals/ast.rs:10 定义的 Container<'a> 和 serde_derive/src/internals/attr.rs:155 的 Container 是两个不同结构——

// 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.3 ctxt.rs:panic-on-drop 强制错误检查

serde_derive/src/internals/ctxt.rs:61-67 的 Drop 实现值得单独写一节:

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() 消费——直接 panic。这是宏运行时的自检(不是编译期检查),防的是 serde_derive 自己的代码漏调 check()。check() 内部用 take() 把 errors 从 Some(Vec) 设成 None(line 13-14 的字段注释明确说:"The contents will be set to None during checking. This is so that checking can be enforced."),让 drop 时通过检测。

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

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

这种"用 Drop 强制 API 调用顺序"的模式——常被称作 drop bomb,是在运行时近似模拟线性类型(Rust 没有线性类型,编译器不会强制你调用 check(),只能靠 drop 时 panic 兜底)。同类的 Drop 设计:tokio 的 JoinHandle 不 await 时不 panic(默认不强制)、但 sqlx 的 Transaction 不 commit 就 drop 时会发起回滚(sqlx-core/src/transaction.rs:260-272)——Drop 的语义可以承载丰富的 API 契约。

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

serde_derive/src/internals/name.rs:9-15 的 MultiName——核心数据结构(下面标了「简化」:源码里字段是 pub(crate),注释为本专栏所加):

// 简化
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 更准确表达"是否被用户主动改过"。

这种"区分默认值和用户值"的设计在配置系统里很重要——只要存在"继承规则可以改写、用户显式值不能改写"的层级覆盖,就得记下值的来源。

11.10 和 parse_nested_meta 的关系

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

为什么 serde 不自己写 parser? 历史上(syn 1.x 时代)attr.rs 是先用 attr.parse_meta() 拿到 syn::Meta 树、再对 NestedMeta 逐层做 match。syn 2.0 引入 parse_nested_meta 后,serde 在 1.0.157 迁移到它(PR serde-rs/serde#2406 "Rewrite attribute parser using parse_nested_meta")。直接背景是:同一个版本里 serde 先升级到了 syn 2(PR #2405 "Update to syn 2"),而 syn 2 删掉了 Attribute::parse_meta 和 NestedMeta(对比 syn 1.0.109 与 2.0.0 的 src/attr.rs),旧写法已经无路可走。提交本身没有写动机说明,从代码看,新写法的好处有两点:

  1. 出错时直接 meta.error(...),错误自带当前 meta 的 span。
  2. 不用再先物化一整棵 Meta 树再匹配,代码更直接。

这也是一次工程上的"借东风"——serde 的长期维护者 dtolnay 同时是 syn 的作者:syn 2.0.0 与 serde 1.0.157 在 2023-03-17 同一天、相隔约一小时(两个仓库里 release 提交的时间)。

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

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

例子 A:无特殊属性

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

生成(精简,serde 1.0.228 实测展开):

let mut __serde_state = _serde::Serializer::serialize_struct(
    __serializer,
    "User",
    false as usize + 1 + 1,
)?;
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "id", &self.id)?;
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "name", &self.name)?;
_serde::ser::SerializeStruct::end(__serde_state)

例子 B:rename_all

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

生成(只列字段部分):

_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "userId", &self.user_id)?;
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "displayName", &self.display_name)?;

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

例子 C:skip_serializing_if

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

生成:

let mut __serde_state = _serde::Serializer::serialize_struct(
    __serializer,
    "User",
    false as usize + 1 + if Option::is_none(&self.email) { 0 } else { 1 },
)?;
_serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "id", &self.id)?;
if !Option::is_none(&self.email) {
    _serde::ser::SerializeStruct::serialize_field(&mut __serde_state, "email", &self.email)?;
} else {
    _serde::ser::SerializeStruct::skip_field(&mut __serde_state, "email")?;
}
_serde::ser::SerializeStruct::end(__serde_state)

变化:字段数变成一个把 skip_serializing_if 结果折进去的运行时表达式(ser.rs 里 fold 出来的 1 + if ... { 0 } else { 1 }),然后对 email 字段用 if 包裹。如果为 None 则 skip_field(而不是不处理)——因为某些格式需要知道"有这个字段但被跳过"。

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

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

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

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

《MCP 协议设计与实现》专栏第 3 章里讨论过 JSON-RPC 的消息格式(那一章给的是 TypeScript / Python SDK 的定义)——method 做 tag、params 做 content,用 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. 一致性检查:单个 attrs 对象内的冲突在 attr.rs 解析时就报掉,跨对象的组合交给 check.rs(477 行)。
  5. 命名转换:case.rs 200 行实现 8 种 rename 风格。
  6. 继承传播:Container 的 rename_all 作用于变体名(enum)或字段名(struct),变体内字段由变体的 rename_all 或容器的 rename_all_fields 决定,用户显式 override 优先。

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

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

动手实验

  1. 阅读 attr.rs 第 237-545 行(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_field 和 apply_to_variant,走的是两段不同的代码,结果却都是 userId——起点不同、殊途同归(见 §11.6.2)。自己推导一遍,再用 cargo expand 验证。

延伸阅读

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