Serde 元编程

第 2 章 Serde Data Model:29 种原语的设计哲学

作者 杨艺韬 · 9,355 字 · 发布于 · 更新于

2.1 为什么需要一套"中间语言"

上一章我们说过,Serde 用一个中间层击碎了 M×N 问题。这个中间层叫 Data Model。但"中间层"这个词太抽象了,不够具体——中间层到底长什么样?谁规定了它的边界?为什么不是 30 种原语或 20 种,恰好是 29 种?

本章要回答这些问题。在此之前,先建立一个更清晰的比喻。

Data Model 的角色很像翻译接力中的"通用中间语"。想象一个联合国会议,有 30 种语言(代表 30 种数据结构),会议要翻译成 5 种官方语言(代表 5 种格式)。如果每一对"源语言-目标语言"都配一个专职翻译,你需要 30 × 5 = 150 个翻译。这就是 M×N。

更聪明的做法是约定一种"中间语"——所有源语言先翻译成中间语,中间语再翻译到目标语言。这样你只需要 30 + 5 = 35 个翻译。但前提是:中间语必须足够丰富,能表达所有源语言的意思,也必须足够通用,让所有目标语言都能接收。

Serde 的 Data Model 就是这种中间语。它要同时满足两个矛盾的约束:

  • 表达力:Rust 类型系统里所有"可序列化"的东西,都得能用 29 种原语表达。
  • 格式无关:每一种原语都不能带某个具体格式的假设。比如不能规定"字符串必须加引号、做转义"(那是 JSON 文本的写法,Bincode 只写长度加原始字节),也不能规定"整数必须变长编码"(JSON 是文本)。

这两个约束把 Data Model 的选型空间压缩得极窄。Serde 选了 29 个原语,每一个都经过取舍。本章会逐个拆解。

2.2 29 种原语的全景图

先给一张表格,建立全局印象。之后我们逐类深入。

类别 原语 对应 Rust 类型举例 Serializer 方法
布尔 bool bool serialize_bool
有符号整数 i8 / i16 / i32 / i64 / i128 i8..=i128 serialize_i8/..i128
无符号整数 u8 / u16 / u32 / u64 / u128 u8..=u128 serialize_u8/..u128
浮点 f32 / f64 f32, f64 serialize_f32, serialize_f64
字符 char char serialize_char
字符串 str &str, String serialize_str
字节串 bytes &[u8](通过 serde_bytes) serialize_bytes
可选 option Option<T> serialize_none, serialize_some
单元 unit () serialize_unit
单元结构 unit_struct struct Nothing; serialize_unit_struct
单元变体 unit_variant enum E { A } 的 A serialize_unit_variant
新类型结构 newtype_struct struct Millimeters(u8); serialize_newtype_struct
新类型变体 newtype_variant enum E { M(String) } 的 M serialize_newtype_variant
变长序列 seq Vec<T>, HashSet<T> serialize_seq
定长元组 tuple (A, B, C) serialize_tuple
元组结构 tuple_struct struct Pair(i32, i32); serialize_tuple_struct
元组变体 tuple_variant enum E { V(A, B) } 的 V serialize_tuple_variant
键值映射 map HashMap<K, V> serialize_map
结构体 struct struct User { ... } serialize_struct
结构变体 struct_variant enum E { V { a: A } } 的 V serialize_struct_variant

数一下:5 种有符号整数 + 5 种无符号整数 + 2 种浮点 + bool + char + str + bytes + option + unit/unit_struct/unit_variant + newtype_struct/newtype_variant + seq/tuple/tuple_struct/tuple_variant + map/struct/struct_variant = 29 种。

设计意图:为什么 serialize_none 和 serialize_some 是 option 这一个原语下的两个调用,而不是两个独立原语?因为它们在语义上表达的是"存在/不存在"这个同一个概念的两种取值。同样的逻辑也适用于后面会看到的 seq 状态机(用 serialize_seq + 多次 serialize_element + end)——一个原语对应一种语义抽象,而不是一个方法对应一种抽象。

Serializer 的真实 trait 定义见 serde_core/src/ser/mod.rs:355(serde_core crate,位于 serde 仓库内;本章下文 serde_core/src/... 路径同此):

// serde_core/src/ser/mod.rs:355
pub trait Serializer: Sized {
    type Ok;
    type Error: Error;
    type SerializeSeq: SerializeSeq<Ok = Self::Ok, Error = Self::Error>;
    type SerializeTuple: SerializeTuple<Ok = Self::Ok, Error = Self::Error>;
    // ... 另外 5 个 SerializeXxx 关联类型

    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error>;
    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error>;
    // ... 一共 30 个 serialize_* 方法(option 占 2 个)
}

注意 7 个关联类型 (SerializeSeq, SerializeTuple, ...)。它们对应 Data Model 里所有"复合"原语——seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant。这 7 种原语的共同特点是:它们不是一次调用就能序列化完的,需要"先开始、中间多次添加、最后结束"的状态机模式。关联类型就是这台状态机的"状态变量"。第 3 章会详细讲状态机。

2.3 数值与基本类型(12+2)

最没有悬念的一组:12 种数值 + bool + char。它们之所以分这么细,有三个原因:

原因一:保留类型信息让格式优化。 Rust 的 u8 和 i64 在 JSON 文本里写出来没区别,但在 Bincode 里一个占 1 字节、一个占 8 字节。Serde 把 12 种数值都拆开,格式实现者可以针对每种类型写最紧凑的编码。

原因二:避免精度损失。 Rust 有 f32 和 f64 两种浮点。如果只给 serialize_float,实现者不知道原始精度,可能把 f32 序列化成"看起来精确"的 f64。精度信息必须保留。

原因三:i128/u128 是特殊的。 Rust 1.26 才稳定了 128 位整数,JSON、MessagePack 等历史悠久的格式没有原生 128 位类型。Serde 给 serialize_i128/serialize_u128 提供了默认实现(返回错误),让不支持 128 位的格式可以明确拒绝:

// serde_core/src/ser/mod.rs:531
fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
    let _ = v;
    Err(Error::custom("i128 is not supported"))
}

char 为什么独立? 因为 Rust 的 char 是 4 字节的 Unicode 标量值,和 u32 虽然位宽相同但语义不同。文本格式(JSON)把 char 写成单字符字符串,二进制格式可能按 Unicode 代码点存。如果把 char 混进 serialize_u32,格式丢失了语义信息。

bytes 为什么不是 Vec<u8>? 这是一个让无数人困惑的设计。Rust 里 Vec<u8> 和 &[u8] 是最自然的"字节数组"表达,但 Serde 默认把它们当作普通的 Vec<T>/[T],不做任何特殊处理([T] 的实现见 serde_core/src/ser/impls.rs:175-184,Vec<T> 由同文件 seq_impl! 宏展开,函数体都是 serializer.collect_seq(self))——经 collect_seq 的默认实现走 serialize_seq,每个字节调一次 serialize_u8。

这是对的吗?在 JSON 里无所谓——serde_json 的 serialize_bytes 同样写成整数数组(serde_json/src/ser.rs:1804-1817 的 write_byte_array),两条路径输出完全相同。Bincode 1.x 和 Postcard 里输出也相同:seq 路径写长度再逐个写 1 字节的 u8,bytes 路径写长度再整块写入原始字节,差别只在逐元素调用的开销。真正拉开差距的是 MessagePack(以 rmp-serde 1.3.1 实测 1024 字节的 Vec<u8>):

  • 走 seq:数组头 + 每个字节单独编码成整数——0~127 占 1 字节,128~255 要加 0xcc 前缀占 2 字节,最坏情况(全是 ≥128 的字节)编码成 2052 字节
  • 走 bytes:bin 头 + 原始字节整体存放,恒为 1028 字节

Serde 的解决方案是一个叫 serde_bytes 的辅助 crate,提供 Bytes/ByteBuf 包装类型和 #[serde(with = "serde_bytes")] 属性,让用户显式选择 bytes 路径。serde_bytes 的 README 把原因归结为 Rust 没有 specialization(特化,尚未稳定):没有它,Serde 只能把 &[u8] 当作普通切片、把 Vec<u8> 当作普通向量处理。

设计意图:bytes 和 seq 分开,是 Serde "格式无关"原则的一次轻微妥协。纯粹的格式无关做法是只有 seq,让格式自己去判断元素类型是不是 u8。但现实中几乎所有二进制格式都对字节串有原生支持,Serde 把它提升为原语,让格式可以用一个方法就接住——性能优先压倒了极致的简洁。

2.4 Option:为什么不是一个 Tag?

Option<T> 是 Rust 最常见的类型之一。Serde 给它一个专门的原语 option,通过两个方法表达:

// serde_core/src/ser/mod.rs:788 & 821
fn serialize_none(self) -> Result<Self::Ok, Self::Error>;

fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
    T: ?Sized + Serialize;

为什么不按 Rust enum 的朴素思路,把 Option 当成一个普通 enum 处理?毕竟 Option<T> 就是 enum Option<T> { None, Some(T) },可以用 unit_variant(None)+ newtype_variant(Some(T))表达。

答案:Option 太常用了,值得特殊优化。 如果走 enum 路径,JSON 里 Some(42) 会编码成 {"Some":42} 这种丑陋的形式。而约定俗成地,所有 JSON 格式都希望 Some(42) 就是 42、None 就是 null。Serde 把 option 单独提出来,让每种格式决定怎么处理"存在性"这个语义,而不受普通 enum 规则的束缚。

不同格式的 option 处理:

格式 None 编码 Some(42u64) 编码
JSON null 42
MessagePack 0xc0 (nil) 0x2a (整数 42)
Bincode 1.x 0x00 (1 字节 tag) 0x01 <42 的 8 字节>
Postcard 0x00 0x01 <42 的 varint>

每种格式都用它自己最紧凑的方式表达"存在"。JSON 甚至完全省略了"存在标签"——直接写值。如果没有独立的 option 原语,无法做到这种格式特异性优化。

设计意图:Option 的单独支持是 Serde "通用抽象 + 实用优化"平衡的典型例子。Data Model 在绝大多数地方追求简洁统一,但在极其常用的模式上愿意增加特殊原语。这种"80/20 取舍"贯穿 Serde 设计——bytes、option、unit 都是同样的原因。

2.5 Unit 家族:三种"没有数据"的形态

Data Model 里有三个看起来差不多的原语:

  • unit:对应 Rust 的 ()(空元组)
  • unit_struct:对应 struct Nothing;
  • unit_variant:对应 enum E { A, B } 中的 A

它们在运行时都不携带任何数据——没有字段、没有值,只有一个"类型身份"。为什么要分三个?

答案:名字信息不同。 看 Serializer trait:

// serde_core/src/ser/mod.rs:841, 861, 889
fn serialize_unit(self) -> Result<Self::Ok, Self::Error>;

fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error>;

fn serialize_unit_variant(
    self,
    name: &'static str,
    variant_index: u32,
    variant: &'static str,
) -> Result<Self::Ok, Self::Error>;
  • serialize_unit 无参数——它纯粹表示"啥都没有"。
  • serialize_unit_struct 带一个 name 参数——它是"一个叫 X 的啥都没有"。
  • serialize_unit_variant 带 name(enum 名)、variant_index(变体下标)、variant(变体名)——它是"enum X 的第 i 个变体 Y"。

这些名字信息对某些格式很重要。 比如你有一个 enum Status { Active, Banned },序列化 Active 时:

  • JSON 格式可以用 "Active" 这个字符串(利用 variant 名字)
  • Bincode 格式可以用整数 0(利用 variant_index,不必写变体名字符串)
  • MessagePack 可能选择 "Active" 也可能选择 0,由具体实现决定

如果 unit_variant 不把这些信息作为参数传入,格式无法做出选择。反过来,unit 不需要任何名字——() 序列化通常就是"什么都不写"(JSON 里是 null,Bincode 里是 0 字节)。

unit_struct 的特殊用法。 struct Millimeters; 这种单元结构体在业务代码里很少直接用,但它是 类型标签(type tag) 的绝佳载体。想象你有一个 struct Celsius(f64),你把它 derive 成 Serialize 后,JSON 会写 42.5——温度的上下文丢了。如果改成 struct Celsius { tag: CelsiusTag, value: f64 },其中 CelsiusTag 是单元结构体,格式就能拿到 "CelsiusTag" 这个名字。但要注意用不用是格式的选择:serde_json 的 serialize_unit_struct 直接转发到 serialize_unit(serde_json/src/ser.rs:209),写出来是 {"tag":null,"value":42.5};想保留名字得靠格式自己利用 name 参数。unit_struct 原语做的是让这种"只为名字存在"的类型有了一等支持。

设计意图:三个 unit 原语的差异不在运行时数据(都是零),而在编译期元信息(名字、索引)。Serde 把这些元信息通过参数传递给 Serializer,让格式可以选择是否利用它们。这呼应了贯穿 Serde 的一个模式:把 Rust 编译期能获取的所有信息,都完整传递给格式层,由格式层决定取舍。

2.6 Newtype 家族:对"包装"的一等支持

Newtype 模式是 Rust 最常见的设计模式之一——用一个单字段 struct 包装一个原始类型,获得类型安全和新语义:

struct UserId(u64);
struct Email(String);
struct Celsius(f64);

在类型系统里,UserId 和 u64 是不同类型,编译器不会让你把一个 u64 当作 UserId 传递。但在运行时,UserId 的内存布局通常和 u64 一样(只有加上 #[repr(transparent)] 才有保证)。

Serde 给 newtype 一个专门的原语:

// serde_core/src/ser/mod.rs:916
fn serialize_newtype_struct<T>(
    self,
    name: &'static str,
    value: &T,
) -> Result<Self::Ok, Self::Error>
where
    T: ?Sized + Serialize;

为什么需要? 看两种可能的实现选择:

选择 A:把 newtype 当作单字段 tuple_struct。 UserId(42) 序列化成 [42](JSON)。正确但冗余——这个包装层没有语义价值。

选择 B:把 newtype 当作透明包装。 UserId(42) 序列化成 42。内容一致,但类型名丢失。

Serde 的选择:交给格式决定。 serialize_newtype_struct 把 name("UserId")和 value(内部的 42)都传给格式,格式决定是用 A 还是 B:

  • serde_json 默认选 B——JSON 里 UserId(42) 就是 42(透明)
  • postcard 也选 B
  • 如果有需要保留类型名的场景,格式可以选 A(或自定义结构)

这个方法没有默认实现(必须由格式实现),但官方文档注释用一句散文把推荐写法定成了透明包装(serde_core/src/ser/mod.rs:898-900):

Serializers are encouraged to treat newtype structs as insignificant wrappers around the data they contain. A reasonable implementation would be to forward to value.serialize(self).

注意这段文档没有给出 Serializer 侧的示例代码——紧随其后的 doctest(serde_core/src/ser/mod.rs:902-915)演示的是调用方:一个 impl Serialize for Millimeters,在里面调 serializer.serialize_newtype_struct("Millimeters", &self.0)。换句话说,"丢弃 name、直接转发"只是官方建议的默认行为,不是源码里能抄的一段实现。真要在自己的格式里落实,写出来是这样(不是源码原文,是按上面那句建议写的实现示意):

// 不是源码原文:按 ser/mod.rs:898-900 的文字建议写出的实现示意
fn serialize_newtype_struct<T>(
    self,
    _name: &'static str,
    value: &T,
) -> Result<Self::Ok, Self::Error>
where
    T: ?Sized + Serialize,
{
    value.serialize(self)  // 直接转发,丢弃 name
}

serde_json 正是这么做的(serde_json/src/ser.rs:225-230,函数体只有 value.serialize(self) 一行)。

newtype_variant 是 enum 版本的同样思路:

// serde_core/src/ser/mod.rs:950
fn serialize_newtype_variant<T>(
    self,
    name: &'static str,
    variant_index: u32,
    variant: &'static str,
    value: &T,
) -> Result<Self::Ok, Self::Error>
where
    T: ?Sized + Serialize;

对应 enum E { M(String) } 的 M 变体——带一个值的 enum 变体。相比 unit_variant,多了一个 value 参数。

设计意图:newtype 的独立原语是 Serde 对 Rust 习惯用法的一次"特殊优待"。如果 Serde 诞生在一个不使用 newtype 模式的语言里(比如 Python),这个原语可能不会出现。Data Model 不是"数据理论上的最小集合",而是"Rust 生态实际最需要的集合"——这是工程而非数学。

2.7 Sequence 家族:4 种序列

Data Model 有 4 种"元素列表"原语:

  • seq:变长,Vec<T>、HashSet<T>(官方文档称其为 heterogeneous,Data Model 并不要求元素同类型)
  • tuple:定长,元素类型可以不同,(A, B, C)
  • tuple_struct:定长带名字,struct Pair(i32, i32)
  • tuple_variant:enum 变体带元组数据,enum E { V(A, B) }

它们的 Serializer 方法签名:

// serde_core/src/ser/mod.rs:1006
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error>;

// serde_core/src/ser/mod.rs:1062
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error>;

// serde_core/src/ser/mod.rs:1089
fn serialize_tuple_struct(
    self,
    name: &'static str,
    len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error>;

// serde_core/src/ser/mod.rs:1134
fn serialize_tuple_variant(
    self,
    name: &'static str,
    variant_index: u32,
    variant: &'static str,
    len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error>;

注意细节:

seq 的 len 是 Option<usize>。 为什么?因为迭代器链式调用时,最终元素数量可能不知道:iter.filter(...).map(...).collect::<Vec<_>>() 可以提前知道长度,但 iter.filter(...) 单独用时不行。seq 允许 None,让流式序列化成为可能。代价是某些格式(Bincode 需要先写长度)无法处理 None——直接报错,调用方得先把所有元素收集起来。

元组的元数上限是 16。 Serialize 对元组的实现是宏批量展开出来的,不是无限泛型:serde_core/src/ser/impls.rs:397 单独写了 impl<T> Serialize for (T,)(一元组),serde_core/src/ser/impls.rs:404-420 的 tuple_impls! { 2 => ... 16 => ... } 把二元到十六元一次展开完,文档属性也在 serde_core/src/ser/impls.rs:392-396 明写 "This trait is implemented for tuples up to 16 items long."。反序列化一侧同样只展开到十六元(serde_core/src/de/impls.rs:1481 起的 tuple_impls!)。超过 16 元的元组没有 Serialize 实现——这是 Rust 缺变长泛型时代的标准做法,不是 Data Model 的语义上限。

tuple/tuple_struct/tuple_variant 的 len 是 usize。 因为 Rust 的元组类型在编译期就确定了长度,(A, B, C) 永远是 3 个元素。格式可以省略长度字段:JSON 里 [1,2,3] 和 [1,2] 字节不同,但 Bincode 里如果双方都知道长度是 3,就可以不写长度,直接写三个值——节省 8 字节。

四者的"名字参数"差异反映了用法差异:

  • seq:完全匿名,因为 Vec<T> 的"身份"不重要
  • tuple:也匿名,因为 (A, B, C) 是结构性类型,没有名字
  • tuple_struct:有 name,因为 struct Pair(i32, i32) 有类型名
  • tuple_variant:有 enum 名、变体索引、变体名——enum 身份是关键信息

这些名字让格式可以选择编码策略:JSON 可能把 E::V(1, 2) 写成 {"V":[1,2]}(用变体名);Bincode 会写 <variant_index><a><b>——变体名一个字节都不进字节流。

状态机:为什么 seq 返回 SerializeSeq 而不是直接写元素?

serialize_seq 返回一个 Self::SerializeSeq 对象,然后调用方在这个对象上调用 serialize_element 多次、最后 end():

// 典型用法
let mut seq = serializer.serialize_seq(Some(vec.len()))?;
for item in &vec {
    seq.serialize_element(item)?;
}
seq.end()

为什么要这个状态机?因为某些格式的"开始"和"结束"需要特殊处理:

  • JSON 需要写 [,然后元素之间加 ,,最后写 ]
  • Bincode 需要在开始时先写长度头(len 为 None 时直接报错)
  • MessagePack 需要在开始时写"数组头"带长度

如果 serialize_seq 一次性接收所有元素(&[T]),就没法处理迭代器场景;如果它每次调用都独立(serialize_element),又没法管理"开头/结尾"状态。状态机模式是这两个约束的平衡点。

设计意图:状态机模式(begin → add × N → end)是 Serde Data Model 的一个核心模式,不只 seq,后面的 map、struct、struct_variant 都用它。这个模式让 Serde 能支持从 O(1) 内存的流式序列化到 O(n) 内存的一次性序列化的完整谱系。

2.8 Map 家族:3 种键值映射

最后三个原语:

  • map:动态键值映射,HashMap<K, V>、BTreeMap<K, V>
  • struct:键在编译期已知的结构体,struct User { id: u64, name: String }
  • struct_variant:enum 变体里的结构体,enum E { V { a: A } }

它们的方法签名:

// serde_core/src/ser/mod.rs:1188
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error>;

// serde_core/src/ser/mod.rs:1220
fn serialize_struct(
    self,
    name: &'static str,
    len: usize,
) -> Result<Self::SerializeStruct, Self::Error>;

// serde_core/src/ser/mod.rs:1264
fn serialize_struct_variant(
    self,
    name: &'static str,
    variant_index: u32,
    variant: &'static str,
    len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error>;

map vs struct 的关键区别: key 是什么时候决定的?

  • map 的 key 在运行时才知道——HashMap<String, i32> 的实际 key 集合取决于插入了什么
  • struct 的 key 在编译期就知道——User { id, name } 的字段永远是 id 和 name

这个区别决定了:

  • serialize_map 接收 Option<usize>(和 seq 一样,流式友好),但每次 serialize_entry 需要接收一个运行时 key
  • serialize_struct 接收 usize(长度编译期已知),每次 serialize_field 的 key 是 &'static str(编译期字符串)

Bincode 如何利用这个区别? Bincode 对 struct 完全不写字段名——反正编译期双方都知道是 User { id, name },按顺序写 id 的值再写 name 的值就够了。而 map 必须写每个 key-value 对的 key,不然接收方不知道这是什么。

对一个有 10 个字段的结构体,map 编码要把 10 个字段名逐个写进字节流,struct 编码在 Bincode 这类格式里把这部分全部省掉——省下的正是所有字段名的字节数加上各自的长度前缀。这是 Data Model 分层的实际经济价值。

struct_variant:如果 enum 的某个变体是 struct 形态:

enum Event {
    Click { x: i32, y: i32 },       // struct_variant
    KeyPress(char),                  // newtype_variant
    Close,                           // unit_variant
}

Click { x: 10, y: 20 } 会调用 serialize_struct_variant("Event", 0, "Click", 2),然后分别 serialize_field("x", &10)、serialize_field("y", &20)、end()。

2.9 完整的"调用形状图"

到这里,29 种原语都介绍完了。用一张综合的流程图看 Serialize 的整体结构:

flowchart TD
    Start[开始序列化一个值] --> Type{Rust 类型是什么?}
    Type -->|bool/数字/char| Prim[serialize_bool/i*/u*/f*/char<br>一次调用]
    Type -->|&str, String| Str[serialize_str<br>一次调用]
    Type -->|"&[u8] (via serde_bytes)"| Bytes[serialize_bytes<br>一次调用]
    Type -->|Option| Opt{Some or None?}
    Opt -->|None| None[serialize_none]
    Opt -->|Some| Some[serialize_some value]
    Type -->|"()"| Unit[serialize_unit]
    Type -->|struct Nothing| US[serialize_unit_struct]
    Type -->|enum 的单元变体| UV[serialize_unit_variant]
    Type -->|struct X T| NS[serialize_newtype_struct]
    Type -->|enum 的元组变体 只 1 个字段| NV[serialize_newtype_variant]
    Type -->|Vec/HashSet 等变长序列| Seq[serialize_seq<br>然后多次 serialize_element<br>最后 end]
    Type -->|A,B,C 元组| Tup[serialize_tuple<br>然后多次 serialize_element<br>最后 end]
    Type -->|struct Pair int,int| TS[serialize_tuple_struct<br>多次 field + end]
    Type -->|enum 的元组变体| TV[serialize_tuple_variant<br>多次 field + end]
    Type -->|HashMap/BTreeMap| Map[serialize_map<br>多次 key/value + end]
    Type -->|struct User field:T| Struct[serialize_struct<br>多次 serialize_field + end]
    Type -->|enum 的结构变体| SV[serialize_struct_variant<br>多次 serialize_field + end]
    style Prim fill:#e0ffe0
    style Str fill:#e0ffe0
    style Bytes fill:#e0ffe0
    style None fill:#e0ffe0
    style Some fill:#e0ffe0
    style Unit fill:#e0ffe0
    style US fill:#e0ffe0
    style UV fill:#e0ffe0
    style NS fill:#e0ffe0
    style NV fill:#e0ffe0
    style Seq fill:#fff0e0
    style Tup fill:#fff0e0
    style TS fill:#fff0e0
    style TV fill:#fff0e0
    style Map fill:#fff0e0
    style Struct fill:#fff0e0
    style SV fill:#fff0e0

绿色节点是一次调用完成的原语;橙色节点是需要状态机(begin → N × add → end)的原语。这个二分法贯穿整个 Data Model。

2.10 Data Model 的镜像面:Deserializer 与 Visitor

到目前为止我们一直从"写出去"的方向讲 Data Model——Serializer 的 30 个 serialize_* 方法如何把 Rust 值拆解到 29 种原语里。但 Data Model 是双向的:同样这 29 种原语也定义了反序列化的词汇表。反向这条链的 trait 在 serde_core/src/de/mod.rs:945:

pub trait Deserializer<'de>: Sized {
    type Error: Error;

    /// 告诉 Deserializer:我不知道是什么类型,请你决定。
    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where V: Visitor<'de>;

    fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where V: Visitor<'de>;
    fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
    where V: Visitor<'de>;
    // ... 加上 deserialize_string / byte_buf / identifier / ignored_any 等提示方法,
    // 一共 31 个 deserialize_*(4 种 enum 变体合并进 deserialize_enum)

    fn is_human_readable(&self) -> bool { true }   // serde_core/src/de/mod.rs:1253
}

乍看结构和 Serializer 是对称的——同样一堆 deserialize_bool / deserialize_i8 / deserialize_str。但两个方向的"谁驱动谁"完全不同,这是初学者最常栽的跟头:

  • 序列化方向:是类型驱动格式。Vec<u8>::serialize 里 Rust 代码主动选择调用 serialize_seq(经 collect_seq)。格式只是被动执行。
  • 反序列化方向:是类型提示格式、但格式决定Visitor。Vec<u8>::deserialize 调用 deserializer.deserialize_seq(visitor)(VecVisitor,serde_core/src/de/impls.rs:1143、:1175)——"seq" 只是提示,真正决定 Visitor 哪个方法被回调的,是格式里实际存的是什么。如果用户要反序列化 i64、JSON 里存的是 42,serde_json 回调的是 visit_u64 而不是 visit_i64(非负整数一律按 u64 解析,serde_json/src/de.rs:118-129),由 Visitor 里写的逻辑决定接不接受这种"宽松对齐"。反过来,格式也可以不给 Visitor 表态的机会:输入是字符串 "42" 时,serde_json 在 deserialize_i64 里直接报 invalid type,根本不回调 visit_str(serde_json/src/de.rs:319-343)。

所以 Data Model 在反序列化方向落到 Visitor 这个第二 trait 上(serde_core/src/de/mod.rs:1317):

pub trait Visitor<'de>: Sized {
    type Value;

    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result;

    fn visit_bool<E: Error>(self, v: bool) -> Result<Self::Value, E> { ... }
    fn visit_i8<E: Error>(self, v: i8)     -> Result<Self::Value, E> { ... }
    fn visit_str<E: Error>(self, v: &str)  -> Result<Self::Value, E> { ... }
    // ... 一共 27 个 visit_* 回调
    fn visit_borrowed_str<E: Error>(self, v: &'de str) -> Result<Self::Value, E> { ... }
    fn visit_borrowed_bytes<E: Error>(self, v: &'de [u8]) -> Result<Self::Value, E> { ... }
}

Visitor 的一个额外信号:visit_borrowed_str/visit_borrowed_bytes 带 'de lifetime——这是 Serde 著名的 zero-copy 反序列化的钩子。&'de str 直接指向原始输入缓冲,不拷贝一字节。格式只有在原始数据本身就是 UTF-8 连续字节(如 JSON string 没有转义)且它的生命周期至少和 'de 一样长时,才调这个方法;否则退回到 visit_str(&str)——生命周期短于 'de,Visitor 想借走就得拷贝。

这个双 trait 设计的工程意义:把"格式怎么读"和"类型想接什么"彻底解耦。一个 Deserialize<'de> for MyType 的实现者只需要写一次 Visitor、描述"我能接受哪些原语如何转成我",就能匹配所有格式。反之一个 Deserializer 格式实现者只管"我的数据里下一个值是什么原语",不用管目标类型有多少种。

2.10.1 asymmetric:两个默认实现藏在这里

从书写量看 Serializer 和 Deserializer 几乎对称,但有两处不对称容易被忽略,都是 serde_core 的真实源码实现:

第一处——deserialize_i128 / deserialize_u128 有默认实现(serde_core/src/de/mod.rs:991-997,deserialize_u128 同款在 serde_core/src/de/mod.rs:1022-1028):

fn deserialize_i128<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where V: Visitor<'de>,
{
    let _ = visitor;
    Err(Error::custom("i128 is not supported"))
}

这是公开的 deserialize_* 方法里唯二没有强制实现的两个(另有一个 #[doc(hidden)] 的 __deserialize_content_v1 带默认实现,注明 "Not public API",serde_core/src/de/mod.rs:1257-1265)。Serializer 侧的 serialize_i128 是同样的默认——也直接报错(§2.3),两侧对称。工程考量:128 位整数在相当多格式里根本没有原生表示(JSON 数字在多数实现里按 IEEE 754 双精度处理,整数只精确到 53 位;MessagePack 的 int 限 64 位),默认实现让格式实现者不必关心它,用到再说。

第二处——Visitor 几乎所有方法都有默认错误实现,只有 expecting(serde_core/src/de/mod.rs:1343)强制实现。这个设计让你可以"只接受一种原语":

impl<'de> Visitor<'de> for MyBoolVisitor {
    type Value = bool;
    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "a boolean")
    }
    fn visit_bool<E: Error>(self, v: bool) -> Result<bool, E> { Ok(v) }
    // 其他 26 个 visit_* 保留默认——要么转发到更宽的类型(如 visit_i8 → visit_i64),要么返回 invalid_type 错误
}

碰到错误类型时默认实现返回的是结构化错误 Error::invalid_type(Unexpected::Xxx, &self)——它会调 self.expecting() 拼错误消息。这就是你见到 "invalid type: integer 42, expected a boolean" 这种信息的来源,expecting 必须实现就是因为它被这套默认链反复调用。

2.11 deserialize_any 与自描述格式的边界

所有 Deserializer 方法里最特殊的是 deserialize_any——它的意思是 "我不知道下一个值是什么类型,格式你来告诉我"。官方源码注释(serde_core/src/de/mod.rs:950-958)直截了当:

When implementing Deserialize, you should avoid relying on Deserializer::deserialize_any unless you need to be told by the Deserializer what type is in the input. Know that relying on Deserializer::deserialize_any means your data type will be able to deserialize from self-describing formats only, ruling out Postcard and many others.

这把一个容易搞错的边界讲得很清楚:Serde 的格式分两类——

自描述格式(self-describing):每个值前面带类型标签。JSON(字面 tokens)、YAML(缩进 + 类型字面值)、MessagePack(前缀字节指示类型)、CBOR、RON。这类格式能实现 deserialize_any——解析器先看 token、再决定 visit_xxx。

非自描述格式(non-self-describing):类型信息不在字节流里,依赖 schema 指导读取。Bincode、Postcard。这类格式的 deserialize_any 通常直接返回错误——因为解析器看到一串字节,没有 schema 就不知道这是个 u32 还是个 string。

实际影响:serde_json::Value、toml::Value 这类"任意结构"容器的反序列化内部靠 deserialize_any——这就是为什么你能 serde_json::from_str::<serde_json::Value>(s) 但不能 postcard::from_bytes::<postcard::Value>(bytes)(Postcard 根本没有 Value 类型)。设计你自己的 Deserialize 实现时,避免调 deserialize_any——一用就把 Bincode、Postcard 这类非自描述格式排除在外。明确调 deserialize_i64 / deserialize_str / deserialize_struct,该 flavor 的信息至少让 Postcard、Bincode 这类非自描述格式知道该读多少字节。

2.11.1 is_human_readable——一个 bool 撑起的格式分野

Deserializer trait 末尾还挂着一个不起眼的方法(serde_core/src/de/mod.rs:1253-1255):

// serde_core/src/de/mod.rs:1253-1255
fn is_human_readable(&self) -> bool {
    true
}

默认 true(偏向 JSON/YAML 这种人能读的格式)。Bincode、Postcard 会 override 成 false。

这个 bool 有两个大用途:

1. 紧凑 vs 友好的二选一——像 std::net::IpAddr、SocketAddr 这类类型在人类可读格式下序列化成字符串("127.0.0.1"),在紧凑格式下序列化成紧凑字节元组([127, 0, 0, 1])。(Duration 不在此列:它两种格式下都走 serialize_struct("Duration", 2),写 secs、nanos 两个字段,serde_core/src/ser/impls.rs:684-695。)同一个 Serialize 实现通过 if serializer.is_human_readable() { ... } else { ... } 分支走不同路径。

2. 格式演进的不可回头承诺——源码注释(serde_core/src/de/mod.rs:1246-1251,serde_core/src/ser/mod.rs:1452-1457 有一份逐字相同的)明确:"modifying this method to change a format from human-readable to compact or vice versa should be regarded as a breaking change"。一旦某个格式声明了自己是 readable 或 compact,之后改变选择就会让已有数据反序列化失败。所以新格式实现这个方法时要提前想清楚。

到这一节为止,我们完成了 Data Model 在两个方向的完整覆盖:Serializer/Deserializer 两棵 trait 对称站立,Visitor 在反序列化方向承担把 29 种原语翻译回 Rust 类型的职责,deserialize_any 和 is_human_readable 是两根连接具体格式能力与类型期望的承诺线。下一章回到序列化方向,完整拆解 Serializer trait,包括它的 7 个状态机子 trait——这是 Data Model 从"静态表格"变成"动态调用序列"的地方。

2.12 Data Model 不包括什么

理解一个抽象的边界,和理解它的内容同样重要。Serde Data Model 明确不支持的东西:

1. 引用和借用关系。 你不能序列化 &'a T 里的"借用信息"——序列化的是被指向的值,引用本身消失。这意味着图结构(有共享节点)无法直接表达,必须手动设计 ID 映射。

2. 裸指针。 *const T、*mut T 没有语义,不能自动序列化。

3. 函数和闭包。 序列化可执行代码是一个独立的巨大问题(远程执行、安全性),不在 Serde 范围内。想序列化回调?用函数名字符串 + 查找表。

4. 类型元数据。 Serde 不能序列化"这是什么类型"本身(只能序列化某个类型的实例)。如果你需要 RTTI,用 typetag 这个第三方 crate。

5. 循环引用。 Rc<RefCell<Node>> 里的循环会导致 serialize 无限递归。Serde 不检测循环——这是用户的责任。注意 serde_json 那道有名的 128 层深度限制帮不上忙:remaining_depth: 128 是 Deserializer 的字段(serde_json/src/de.rs:34 声明、serde_json/src/de.rs:63 初始化),递减与超限判定在 serde_json/src/de.rs:1374-1378,只保护"解析深嵌套输入"这一侧;serde_json/src/ser.rs 全文没有任何深度计数(grep remaining_depth 0 处)。序列化一个成环的 Rc 得到的是栈溢出,不是 Err。

6. 保留 Rust 类型标签。 Vec<u8> 和 Box<[u8]> 序列化结果一样,反序列化时 Data Model 无法区分——字节流里只有"这是一个 seq",没有"这是哪种容器"。

设计意图:Serde 选择不做"万能"序列化器。它聚焦于"树形结构数据"这个最主流的场景,把图结构、RTTI、循环等边缘问题推给第三方库或用户手动处理。这种"做减法"的克制是 Serde 能保持简洁快速的关键。

2.13 Data Model 与具体格式的对应关系

为了把 Data Model 的抽象具象化,看几个真实原语在 4 种主流格式里的编码对比(以下字节均用 serde_json 1.0.149、rmp-serde 1.3.1、bincode 1.3.3 默认配置、postcard 1.1.3 实测)。

例子:User { id: 42u64, name: "alice" }

JSON:

{"id":42,"name":"alice"}

MessagePack(十六进制,rmp_serde::to_vec_named 的输出;默认的 to_vec 会把 struct 编成数组):

82 a2 69 64 2a a4 6e 61 6d 65 a5 61 6c 69 63 65
  • 82 = map with 2 entries
  • a2 69 64 = str of length 2, "id"
  • 2a = integer 42
  • a4 6e 61 6d 65 = str of length 4, "name"
  • a5 61 6c 69 63 65 = str of length 5, "alice"

Bincode:

2a 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 61 6c 69 63 65
  • 2a 00 00 00 00 00 00 00 = u64 42(小端)
  • 05 00 00 00 00 00 00 00 = u64 长度 5
  • 61 6c 69 63 65 = "alice" 的 UTF-8 字节

注意 Bincode 完全没有字段名——因为 struct 原语允许格式省略字段名。

Postcard(类似 Bincode 但用 varint):

2a 05 61 6c 69 63 65
  • 2a = varint 42(1 字节)
  • 05 = varint 5(字符串长度)
  • 61 6c 69 63 65 = "alice"

同一份数据,文本格式占 24 字节,紧凑二进制占 7 字节——差 3.4 倍。这就是 Data Model 层面允许格式做特异化优化的价值:Serde 不预设"应该怎么编码",它只传递结构信息,让每种格式做自己最擅长的事。

2.13.1 实测:is_human_readable 的真实用户——10 处 IP/网络类型

§2.11.1 说 is_human_readable 这个 bool 撑起了格式分野,下面到源码里实测它的真实使用场景——

grep is_human_readable serde_core/src/{ser,de}/impls.rs 共 10 处使用(ser/impls.rs 6 处 + de/impls.rs 4 处)——全部集中在 std::net 的地址类型上:

网络地址类型(std::net)——

Rust 类型 序列化分支(human_readable=true) 二进制分支
IpAddr dispatch 到 Ipv4Addr / Ipv6Addr dispatch 到对应二进制路径
Ipv4Addr "192.0.2.1" 字符串(最长 15 字符) octets() 的 [u8; 4],走 tuple
Ipv6Addr "2001:db8::1" 字符串(最长 39 字符) octets() 的 [u8; 16],走 tuple
SocketAddr dispatch 到 V4/V6 socket 二进制
SocketAddrV4 "192.0.2.1:65000" 字符串(最长 21 字符) ip + port 各自二进制
SocketAddrV6 字符串带 zone 二进制

实测 serde_core/src/ser/impls.rs 里的真实代码——

// serde_core/src/ser/impls.rs:806
impl Serialize for net::Ipv4Addr {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        if serializer.is_human_readable() {
            const MAX_LEN: usize = 15;
            debug_assert_eq!(MAX_LEN, "101.102.103.104".len());
            let mut buf = [b'.'; MAX_LEN];
            let mut written = format_u8(self.octets()[0], &mut buf);
            for oct in &self.octets()[1..] {
                // Skip over delimiters that we initialized buf with
                written += format_u8(*oct, &mut buf[written + 1..]) + 1;
            }
            // Safety: We've only written ASCII bytes to the buffer, so it is valid UTF-8
            let buf = unsafe { str::from_utf8_unchecked(&buf[..written]) };
            serializer.serialize_str(buf)
        } else {
            self.octets().serialize(serializer)   // [u8; 4] → 4 元素 tuple
        }
    }
}

两条值得记住的事实:

  1. is_human_readable 在 std 类型的实现里只有 10 处使用,全是网络地址类型的双格式分支。Serde 没有把这个 bool 撒到每个类型上,只在可读字符串与紧凑字节差别明显的地址类型上分出两条路径。
  2. debug_assert_eq!(MAX_LEN, "101.102.103.104".len()):Ipv4Addr 的序列化代码把 15 字符上限硬编码成栈缓冲区大小,再用 debug 断言核对,避免了一次堆分配。

is_human_readable 默认实现是 true(serde_core/src/ser/mod.rs:1459、serde_core/src/de/mod.rs:1253):不显式实现的 Serializer/Deserializer 都被当作 human readable,所以 bincode、postcard 等二进制格式必须显式 override 为 false 才能让 IP 地址走二进制路径;JSON / YAML / TOML 直接复用默认值。

2.14 本章小结

Data Model 是 Serde 的"中间语"——一套格式无关、类型安全、编译期零开销的数据表示。29 种原语不是随意选的,它们对应:

  • 数值与文本:14 种基本类型(12 数值 + bool + char),加上 str、bytes 共 16 种
  • 可选性:option 一个原语,两次调用
  • 空值身份:unit、unit_struct、unit_variant 三种"没有数据"的语义
  • 透明包装:newtype_struct、newtype_variant 两种一字段形态
  • 有序复合:seq(变长)、tuple(定长)、tuple_struct、tuple_variant 四种序列
  • 键值复合:map(运行时 key)、struct(编译期 key)、struct_variant 三种映射

四个关键设计原则贯穿 Data Model:

  1. 类型信息不丢失:12 种数值分别对应,名字通过参数传递,格式可选择性利用。
  2. 状态机表达复合结构:begin → add → end 模式让流式序列化和一次性序列化共存。
  3. 格式无关但优化友好:原语不预设编码,但传递足够的"提示信息"让格式各显神通。
  4. 双向对称但驱动反转:Serializer 由类型主动驱动调用链,Deserializer 由格式决定回调哪个 Visitor 方法;deserialize_any 明确标出"只适用于自描述格式"的边界,is_human_readable 让同一类型能在紧凑/友好两种编码之间做选择。

动手实验

  1. 观察不同格式的编码差异。写一个简单 struct:
    #[derive(Serialize)]
    struct User { id: u64, name: String }
    分别用 serde_json::to_string、bincode::serialize(bincode 1.x 的 API,依赖写 bincode = "1")、rmp_serde::to_vec 序列化 User { id: 42, name: "alice".into() }。打印字节数和内容。看看哪种格式最紧凑、哪种最可读。
  2. 尝试 bytes 优化。把 Vec<u8> 类型的字段直接 derive 和用 #[serde(with = "serde_bytes")] 分别编码成 MessagePack,对比字节数(字节值全小于 128 时两者一样大,混入 ≥128 的字节后差距才出现,见 §2.3)。
  3. 思考题:如果 Data Model 没有 option 原语,必须用 enum 路径表达 Option<T>,那么 Option<Option<T>> 的 JSON 表示会是什么样?为什么这会让 null 和 missing 字段的区分成为一个问题?

延伸阅读