Appearance
第 2 章 Serde Data Model:29 种原语的设计哲学
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 种原语表达。
- 格式无关:每一种原语都不能带某个具体格式的假设。比如不能规定"字符串必须 UTF-8"(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/serde_core/src/ser/mod.rs:355:
rust
// serde/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 个方法(option 占 2 个方法)
}注意 7 个关联类型 (SerializeSeq, SerializeTuple, ...)。它们对应 Data Model 里所有"复合"原语——seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant。这 7 种原语的共同特点是:它们不是一次调用就能序列化完的,需要"先开始、中间多次添加、最后结束"的状态机模式。关联类型就是这台状态机的"状态变量"。第 3 章会详细讲状态机。
2.3 数值与基本类型(14+2)
最没有悬念的一组:14 种数值 + bool + char。它们之所以分这么细,有三个原因:
原因一:保留类型信息让格式优化。 Rust 的 u8 和 i64 在 JSON 文本里写出来没区别,但在 Bincode 里一个占 1 字节、一个占 8 字节。Serde 把 14 种数值都拆开,格式实现者可以针对每种类型写最紧凑的编码。
原因二:避免精度损失。 Rust 有 f32 和 f64 两种浮点。如果只给 serialize_float,实现者不知道原始精度,可能把 f32 序列化成"看起来精确"的 f64。精度信息必须保留。
原因三:i128/u128 是特殊的。 Rust 1.26 才稳定了 128 位整数,JSON、MessagePack 等历史悠久的格式没有原生 128 位类型。Serde 给 serialize_i128/serialize_u128 提供了默认实现(返回错误),让不支持 128 位的格式可以明确拒绝:
rust
// serde/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] 的特化——走 serialize_seq,每个字节调一次 serialize_u8。
这是对的吗?在 JSON 里无所谓——反正都是文本。但在 MessagePack 里,差别巨大:
- 走
seq:每个字节前后都有额外的类型标签,1KB 数据变成 1KB + N 个标签 - 走
bytes:整体作为一个二进制 blob 存,最紧凑
Serde 的解决方案是一个叫 serde_bytes 的辅助 crate,提供 #[serde(with = "serde_bytes")] 属性,让用户显式选择 bytes 路径。未来(尚未稳定)有 specialization 特性时,Serde 可以为 &[u8] 自动优化。
设计意图:
bytes和seq分开,是 Serde "格式无关"原则的一次轻微妥协。纯粹的格式无关做法是只有seq,让格式自己去判断元素类型是不是 u8。但现实中几乎所有二进制格式都对字节串有原生支持,Serde 把它提升为原语,让格式可以用一个方法就接住——性能优先压倒了极致的简洁。
2.4 Option:为什么不是一个 Tag?
Option<T> 是 Rust 最常见的类型之一。Serde 给它一个专门的原语 option,通过两个方法表达:
rust
// serde/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(42) 编码 |
|---|---|---|
| JSON | null | 42 |
| MessagePack | 0xc0 (nil) | 0x2a (整数 42) |
| Bincode | 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:
rust
// serde/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,省 5 字节) - 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 是单元结构体,JSON 就能写 {"tag":"CelsiusTag","value":42.5}——类型信息保留。unit_struct 原语让这种"只为名字存在"的类型有了一等支持。
设计意图:三个 unit 原语的差异不在运行时数据(都是零),而在编译期元信息(名字、索引)。Serde 把这些元信息通过参数传递给 Serializer,让格式可以选择是否利用它们。这呼应了贯穿 Serde 的一个模式:把 Rust 编译期能获取的所有信息,都完整传递给格式层,由格式层决定取舍。
2.6 Newtype 家族:对"包装"的一等支持
Newtype 模式是 Rust 最常见的设计模式之一——用一个单字段 struct 包装一个原始类型,获得类型安全和新语义:
rust
struct UserId(u64);
struct Email(String);
struct Celsius(f64);在类型系统里,UserId 和 u64 是不同类型,编译器不会让你把 i 当作 UserId 传递。但在运行时,UserId 的内存布局和 u64 完全一样——repr(transparent)。
Serde 给 newtype 一个专门的原语:
rust
// serde/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(或自定义结构)
默认实现就是透明包装:
rust
// serde/serde_core/src/ser/mod.rs(文档示例)
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self) // 直接转发,丢弃 name
}newtype_variant 是 enum 版本的同样思路:
rust
// serde/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>tuple:定长,元素类型可以不同,(A, B, C)tuple_struct:定长带名字,struct Pair(i32, i32)tuple_variant:enum 变体带元组数据,enum E { V(A, B) }
它们的 Serializer 方法签名:
rust
// serde/serde_core/src/ser/mod.rs:1006
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error>;
// serde/serde_core/src/ser/mod.rs:1062
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error>;
// serde/serde_core/src/ser/mod.rs:1089
fn serialize_tuple_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error>;
// serde/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,得把所有元素先收集起来。
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>(省 6 字节)。
状态机:为什么 seq 返回 SerializeSeq 而不是直接写元素?
serialize_seq 返回一个 Self::SerializeSeq 对象,然后调用方在这个对象上调用 serialize_element 多次、最后 end():
rust
// 典型用法
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 } }
它们的方法签名:
rust
// serde/serde_core/src/ser/mod.rs:1188
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error>;
// serde/serde_core/src/ser/mod.rs:1220
fn serialize_struct(
self,
name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error>;
// serde/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需要接收一个运行时 keyserialize_struct接收usize(长度编译期已知),每次serialize_field的 key 是&'static str(编译期字符串)
Bincode 如何利用这个区别? Bincode 对 struct 完全不写字段名——反正编译期双方都知道是 User { id, name },按顺序写 id 的值再写 name 的值就够了。而 map 必须写每个 key-value 对的 key,不然接收方不知道这是什么。
对一个有 10 个字段的结构体,map 编码每个字段都要写字段名(20-100 字节),struct 编码可以省 200-1000 字节。这是 Data Model 分层的实际经济价值。
struct_variant:如果 enum 的某个变体是 struct 形态:
rust
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 的整体结构:
绿色节点是一次调用完成的原语;橙色节点是需要状态机(begin → N × add → end)的原语。这个二分法贯穿整个 Data Model。
2.10 Data Model 的镜像面:Deserializer 与 Visitor
到目前为止我们一直从"写出去"的方向讲 Data Model——Serializer 的 30 个方法如何把 Rust 值拆解到 29 种原语里。但 Data Model 是双向的:同样这 29 种原语也定义了反序列化的词汇表。反向这条链的 trait 在 serde_core/src/de/mod.rs:945:
rust
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>;
// ... 对应 29 种原语各一个方法,加上 deserialize_any 一共 30+ 个
fn is_human_readable(&self) -> bool { true } // line 1253
}乍看结构和 Serializer 是对称的——同样一堆 deserialize_bool / deserialize_i8 / deserialize_str。但两个方向的"谁驱动谁"完全不同,这是初学者最常栽的跟头:
- 序列化方向:是类型驱动格式。
Vec<u8>::serialize里 Rust 代码主动选择调用serialize_seq。格式只是被动执行。 - 反序列化方向:是类型提示格式、但格式决定Visitor。
Vec<u8>::deserialize调用deserializer.deserialize_seq(MyVisitor)——"seq" 只是提示,真正决定 Visitor 哪个方法被回调的,是格式里实际存的是什么。如果用户要反序列化i64但文件里存的是字符串"42",JSON Deserializer 会回调visit_str而不是visit_i64,由 Visitor 里写的逻辑决定接不接受这种"宽松对齐"。
所以 Data Model 在反序列化方向落到 Visitor 这个第二 trait 上(serde_core/src/de/mod.rs:1317):
rust
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> { ... }
// ... 对应 29 种原语的 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、描述"我能接受哪些原语如何转成我",就能匹配所有 30+ 种格式。反之一个 Deserializer 格式实现者只管"我的数据里下一个值是什么原语",不用管目标类型有多少种。
2.10.1 asymmetric:两个默认实现藏在这里
从书写量看 Serializer 和 Deserializer 几乎对称,但有两处不对称容易被忽略,都是 serde_core 的真实源码实现:
第一处——deserialize_i128 / deserialize_u128 有默认实现(line 988–997):
rust
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 方法。Serializer 侧的 serialize_i128 有类似的默认(落到 serialize_i64 失败时退化),但 Deserializer 侧是直接报错。工程考量:128 位整数在相当多格式里根本没有原生表示(JSON 浮点最多 53 位精度、MessagePack 的 int 限 64 位),默认实现让格式实现者不必关心它,用到再说。
第二处——Visitor 几乎所有方法都有默认错误实现,只有 expecting 强制实现。这个设计让你可以"只接受一种原语":
rust
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) }
// 其他 28 个方法保留默认——全部返回 InvalidType 错误
}碰到错误类型时默认实现返回的是结构化错误 Error::invalid_type(Unexpected::Xxx, &self)——它会调 self.expecting() 拼错误消息。这就是你见到 "invalid type: integer 42, expected a boolean" 这种信息的来源,expecting 必须实现就是因为它被这套默认链反复调用。
2.11 deserialize_any 与自描述格式的边界
所有 Deserializer 方法里最特殊的是 deserialize_any——它的意思是 "我不知道下一个值是什么类型,格式你来告诉我"。官方源码注释(line 950–958)直截了当:
When implementing
Deserialize, you should avoid relying onDeserializer::deserialize_anyunless you need to be told by the Deserializer what type is in the input. Know that relying onDeserializer::deserialize_anymeans 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、FlexBuffers with schema。这类格式的 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——一用就把适用格式削到一半。明确调 deserialize_i64 / deserialize_str / deserialize_struct,该 flavor 的信息至少让 Postcard、Bincode 这类非自描述格式知道该读多少字节。
2.11.1 is_human_readable——一个 bool 撑起的格式分野
Deserializer trait 末尾还挂着一个不起眼的方法(line 1253):
rust
fn is_human_readable(&self) -> bool { true }默认 true(偏向 JSON/YAML 这种人能读的格式)。Bincode、Postcard 会 override 成 false。
这个 bool 有两个大用途:
1. 紧凑 vs 友好的二选一——像 std::net::IpAddr、SocketAddr、Duration 这类类型在人类可读格式下序列化成字符串("127.0.0.1"、"2s"),在紧凑格式下序列化成紧凑字节元组([127, 0, 0, 1]、(2, 0))。同一个 Serialize 实现通过 if serializer.is_human_readable() { ... } else { ... } 分支走不同路径。
2. 格式演进的不可回头承诺——源码注释(line 1248–1251)明确:"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 的 7 种状态机——这是 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 在深度超过一定值时会报错,算是一层保护。
6. 保留 Rust 类型标签。 Vec<u8> 和 Box<[u8]> 序列化结果一样,反序列化时 Data Model 无法区分。这也是 bytes 原语为什么需要显式标记。
设计意图:Serde 选择不做"万能"序列化器。它聚焦于"树形结构数据"这个 95% 的场景,把图结构、RTTI、循环等边缘问题推给第三方库或用户手动处理。这种"做减法"的克制是 Serde 能保持简洁快速的关键。
2.13 Data Model 与具体格式的对应关系
为了把 Data Model 的抽象具象化,看几个真实原语在 5 种主流格式里的编码对比。
例子:User { id: 42u64, name: "alice" }
JSON:
json
{"id":42,"name":"alice"}MessagePack(十六进制):
82 a2 69 64 2a a4 6e 61 6d 65 a5 61 6c 69 63 6582= map with 2 entriesa2 69 64= str of length 2, "id"2a= integer 42a4 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 652a 00 00 00 00 00 00 00= u64 42(小端)05 00 00 00 00 00 00 00= u64 长度 561 6c 69 63 65= "alice" 的 UTF-8 字节
注意 Bincode 完全没有字段名——因为 struct 原语允许格式省略字段名。
Postcard(类似 Bincode 但用 varint):
2a 05 61 6c 69 63 652a= 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 的真实用户——9 处 IP/网络类型
§2.11.1 标题"is_human_readable——一个 bool 撑起的格式分野"——把这个 bool 的真实使用场景在源码里实测一次——
grep is_human_readable serde_core/src/{ser,de}/impls.rs 共 10 处使用——全部集中在两类:
网络地址类型(std::net)——
| Rust 类型 | 序列化分支(human_readable=true) | 二进制分支 |
|---|---|---|
IpAddr | dispatch 到 Ipv4Addr / Ipv6Addr | dispatch 到对应二进制路径 |
Ipv4Addr | "192.0.2.1" 字符串(最长 15 字符) | 4 字节大端 |
Ipv6Addr | "2001:db8::1" 字符串(最长 39 字符) | 16 字节 |
SocketAddr | dispatch 到 V4/V6 socket | 二进制 |
SocketAddrV4 | "192.0.2.1:65000" 字符串(最长 21 字符) | ip + port 各自二进制 |
SocketAddrV6 | 字符串带 zone | 二进制 |
实测 serde_core/src/ser/impls.rs 里的真实代码——
rust
// serde_core/src/ser/impls.rs(实测)
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];
serialize_display_bounded_length!(self, MAX_LEN, serializer)
} else {
// 4 字节直接 serialize_bytes
}两条值得记住的物理事实——
is_human_readable在 std 类型里仅 9 处使用——全是网络地址类型的双格式分支——Serde 没把这个 bool 撒到处都用——印证 §2.11.1 "一个 bool 撑起的格式分野" 的"克制"含义:不是每个类型都需要双路径,只在压缩收益显著的网络地址类型上做差别——这是"优化要在数据点上选择"的工程纪律debug_assert_eq!(MAX_LEN, "101.102.103.104".len())——Ipv4Addr序列化代码里硬编码 15 字符上限作为栈缓冲区大小、用 debug 断言验证不会溢出——是 §2.11 "Serde 在性能上的苛刻" 的具体例:避免堆分配、用栈数组 + 边界证明——和 §3.10.1 测得的tower-layer/tuple.rs330 行手展 16 个 impl 同款"为性能放弃语法糖"风格
is_human_readable 默认实现是 true(ser/mod.rs:1459 + de/mod.rs:1253)——所有不显式实现的 Serializer/Deserializer 都被认为是 human readable——意味着 bincode、postcard 等二进制格式必须显式 override 为 false才能让 IP 地址走二进制路径;JSON / YAML / TOML 直接复用默认值——default 站在用户体验最常见的一边。
2.14 本章小结
Data Model 是 Serde 的"中间语"——一套格式无关、类型安全、编译期零开销的数据表示。29 种原语不是随意选的,它们对应:
- 数值与文本:16 种基本类型(14 数值 + bool + char + str + bytes - 1 重复)
- 可选性:option 一个原语,两次调用
- 空值身份:unit、unit_struct、unit_variant 三种"没有数据"的语义
- 透明包装:newtype_struct、newtype_variant 两种一字段形态
- 有序复合:seq(变长)、tuple(定长)、tuple_struct、tuple_variant 四种序列
- 键值复合:map(运行时 key)、struct(编译期 key)、struct_variant 三种映射
四个关键设计原则贯穿 Data Model:
- 类型信息不丢失:14 种数值分别对应,名字通过参数传递,格式可选择性利用。
- 状态机表达复合结构:begin → add → end 模式让流式序列化和一次性序列化共存。
- 格式无关但优化友好:原语不预设编码,但传递足够的"提示信息"让格式各显神通。
- 双向对称但驱动反转:Serializer 由类型主动驱动调用链,Deserializer 由格式决定回调哪个 Visitor 方法;
deserialize_any明确标出"只适用于自描述格式"的边界,is_human_readable让同一类型能在紧凑/友好两种编码之间做选择。
动手实验
- 观察不同格式的编码差异。写一个简单 struct:rust分别用
#[derive(Serialize)] struct User { id: u64, name: String }serde_json::to_string、bincode::serialize、rmp_serde::to_vec序列化User { id: 42, name: "alice".into() }。打印字节数和内容。看看哪种格式最紧凑、哪种最可读。 - 尝试 bytes 优化。把
Vec<u8>类型的字段直接 derive 和用#[serde(with = "serde_bytes")]分别编码成 MessagePack,对比字节数。 - 思考题:如果 Data Model 没有 option 原语,必须用 enum 路径表达
Option<T>,那么Option<Option<T>>的 JSON 表示会是什么样?为什么这会让 null 和 missing 字段的区分成为一个问题?
延伸阅读
- Serde Data Model 官方文档:本章概念的官方版本。
- serde_bytes 仓库:理解 bytes vs seq 的工程权衡。
- Postcard 格式规范:嵌入式场景下的 Serde 格式,可作为"极简格式"学习样本。
- typetag:补上 Serde Data Model 的 trait object 空白。