Serde 元编程
第 11 章 属性宏系统:#[serde(...)] 的解析机制
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(...)] 标注。它必须兼容、健壮、友好。
本章要做三件事:
- 把 serde 的属性按层级分类组织(逐条清单以官方 Attributes 文档为准,见文末延伸阅读)。
- 看
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,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),
// ...
}
}
}
四步:
- 初始化所有属性变量为
none(表示"还没被设置")。 - 遍历类型上的所有属性(一个类型可能有多个
#[serde(...)])。 - 每个
#[serde(...)]内部遍历每个 meta 项(一个#[serde(a=1, b=2, c)]里有三个 meta)。每个 meta 用if meta.path == XXX分派到对应处理。 - 组装最终 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_deserializingbound反过来被拆成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_identifierenum(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());
}
}
}
// ...
}
}
三层继承:
- 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):
// 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),旧写法已经无路可走。提交本身没有写动机说明,从代码看,新写法的好处有两点:
- 出错时直接
meta.error(...),错误自带当前 meta 的 span。 - 不用再先物化一整棵
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 行代码撑起的用户接口。它的核心组织方式:
- 三个层级:Container / Variant / Field,层级间有继承关系。
- 三个工具:
Attr<T>(最多 1 次)、BoolAttr(0/1)、VecAttr<T>(多次)。 - 解析流程:遍历属性 →
parse_nested_meta分派 → 组装结构体。 - 一致性检查:单个 attrs 对象内的冲突在 attr.rs 解析时就报掉,跨对象的组合交给
check.rs(477 行)。 - 命名转换:
case.rs200 行实现 8 种 rename 风格。 - 继承传播:Container 的
rename_all作用于变体名(enum)或字段名(struct),变体内字段由变体的rename_all或容器的rename_all_fields决定,用户显式 override 优先。
生产级过程宏的"属性系统"就该这么做。如果你写自己的 derive 宏需要属性,照着这个模式组织——一致、可维护、错误信息清晰。
下一章进入 Serialize 代码生成——用 Container 和解析出的属性信息生成 impl Serialize for T 的完整代码。你会看到第 3 章讲的 Serializer 协议如何在这里落地成真实 quote! 模板。
动手实验
- 阅读 attr.rs 第 237-545 行(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,走的是两段不同的代码,结果却都是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 协议定义里。