Appearance
第 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(...)] 标注。它必须兼容、健壮、友好。
本章要做三件事:
- 把 serde 所有属性分类组织,形成完整参考。
- 看
attr.rs如何解析这些属性——Attr<T>、BoolAttr、VecAttr三种工具模式。 - 理解属性的一致性检查(在
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() -> &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):
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> 是所有 "最多出现一次" 类型属性的标准模式。
BoolAttr 是 Attr<()> 的包装:
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_fields、transparent、untagged 等 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_with 和 deserialize_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 identifier(r#type、r#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),
// ...
}
}
}四步:
- 初始化所有属性变量为
none(表示"还没被设置")。 - 遍历类型上的所有属性(一个类型可能有多个
#[serde(...)])。 - 每个
#[serde(...)]内部遍历每个 meta 项(一个#[serde(a=1, b=2, c)]里有三个 meta)。每个 meta 用if meta.path == XXX分派到对应处理。 - 组装最终 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、&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 定义:
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_variant | apply_to_field |
|---|---|---|---|
UserId (variant) | snake_case | user_id | n/a |
user_id (field) | snake_case | n/a | user_id(不变) |
userId (field) | snake_case | n/a | user_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-33 的 RENAME_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::serialize和module::path::deserialize替代默认的字段序列化。flatten:把字段的 struct 内容"展平"到父级 struct。borrow:反序列化时借用'de生命周期的输入数据。
这些属性交互产生了 serde 的"高级用法"——第 16 章会专门讨论 with、flatten、remote、getter 等属性的内部机理。
11.7.1 Field 实测:13 个字段不是 20 个
§11.1 提到 Field 属性"约 20 种"——但 attr.rs:978 的 Field 结构体实际只有 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——同时承载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"两个字段值
这个折叠让 Field 结构体更紧凑——但代价是用户写错时错误信息要"反向追溯"。比如 with 属性的错误消息可能只指向 serialize_with 字段——这在 ergonomics 上是个小妥协。
11.7.2 Variant 实测:11 个字段对应 12+ 种属性
attr.rs:728 的 Variant:
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_tuple | 27 | tuple struct 上的 #[serde(default)] 用法限制 |
check_remote_generic | 66 | #[serde(remote = "Path<T>")] 不能带泛型参数 |
check_getter | 78 | getter 必须配合 remote、且不能用在 newtype struct |
check_flatten | 100 | flatten 字段不能和某些容器属性共存 |
check_flatten_field | 117 | flatten 字段不能用在 tuple struct |
check_identifier | 144 | field_identifier / variant_identifier 的强约束(必须是 enum、variant 不能有字段、等) |
check_variant_skip_attrs | 226 | enum variant 的 skip 属性组合限制(如 untagged + skip 冲突) |
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)] 必须只有一个非 skip 字段(且自动 forward 实现) |
check_from_and_try_from | 470 | from 和 try_from 互斥、不能同时设 |
11 个函数 × 平均 4 处错误点 ≈ 40 次 error_spanned_by。每一次都对应一个生产环境真实出现过的用户错误模式——所以才被加进 check.rs。
check_transparent 是唯一签名带 &mut Container 的——它会在确认合法时把 transparent 标志真正写进 Container(而不是仅在解析时)——这是为了等所有字段处理完、知道最终非 skip 字段数量后才能定论。check 同时承担"验证 + 后处理"双重职责。
11.8.2 TagType enum 的四种形态
attr.rs:178 的 TagType 是 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());
}
}
}
// ...
}
}三层继承:
- Container 的
rename_all_rules影响所有变体名(如果是 enum)和所有字段名(如果是 struct)。 - Container 的
rename_all_fields_rules影响 enum 各变体内部的字段名。 - 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.rs | 218 | 把 syn::DeriveInput 转 serde 自己的 Container/Data/Variant/Field |
name.rs | 113 | MultiName 数据结构(serialize / deserialize 双名 + alias 集合) |
ctxt.rs | 68 | Ctxt 错误累积器(强制 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:155 的 Container 是两个不同结构——
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() 消费——直接 panic。check() 内部把 errors 从 Some(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:113 的 MultiName——核心数据结构:
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 --default 和 git 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 迁移到它——因为:
- 错误信息更好(syn 的默认错误带精准 span)。
- 兼容性更好(支持
=和(...)两种风格)。 - 代码少得多(手写 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 行代码撑起的用户接口。它的核心组织方式:
- 三个层级:Container / Variant / Field,层级间有继承关系。
- 三个工具:
Attr<T>(最多 1 次)、BoolAttr(0/1)、VecAttr<T>(多次)。 - 解析流程:遍历属性 →
parse_nested_meta分派 → 组装结构体。 - 一致性检查:
check.rs里 477 行规则覆盖所有非法组合。 - 命名转换:
case.rs200 行实现 9 种 rename 风格。 - 继承传播:Container 的
rename_all传到 Variant 再传到 Field,用户显式 override 优先。
生产级过程宏的"属性系统"就该这么做。如果你写自己的 derive 宏需要属性,照着这个模式组织——一致、可维护、错误信息清晰。
下一章进入 Serialize 代码生成——用 Container 和解析出的属性信息生成 impl Serialize for T 的完整代码。你会看到第 3 章讲的 Serializer 协议如何在这里落地成真实 quote! 模板。
动手实验
- 阅读 attr.rs 第 237-547 行(Container::from_ast 完整体)。这约 300 行代码用最直白的方式展示了 serde 如何解析 18 种 container 属性。
- 给 serde 加一个新属性(fork 自己试):比如
#[serde(log_on_serialize)],让生成代码在序列化时打印 log。步骤:(a) 在 symbol.rs 加常量;(b) 在 attr::Container 加字段;(c) 在 from_ast 里解析;(d) 在 ser.rs 里用这个字段决定是否生成 log 代码。不完全实现也行,理解流程就好。 - 读 check.rs:随便挑 3 条 check 规则,理解它们为什么存在——写两个会触发这些规则的示例。
- 对比 rename_all 转换效果:
#[serde(rename_all = "camelCase")]对字段名user_id和变体名UserId的转换结果是不同的——它们分别调用apply_to_field和apply_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 协议定义里。