Skip to content

第 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 方法
布尔boolboolserialize_bool
有符号整数i8 / i16 / i32 / i64 / i128i8..=i128serialize_i8/..i128
无符号整数u8 / u16 / u32 / u64 / u128u8..=u128serialize_u8/..u128
浮点f32 / f64f32, f64serialize_f32, serialize_f64
字符charcharserialize_char
字符串str&str, Stringserialize_str
字节串bytes&[u8](通过 serde_bytes)serialize_bytes
可选optionOption<T>serialize_none, serialize_some
单元unit()serialize_unit
单元结构unit_structstruct Nothing;serialize_unit_struct
单元变体unit_variantenum E { A }Aserialize_unit_variant
新类型结构newtype_structstruct Millimeters(u8);serialize_newtype_struct
新类型变体newtype_variantenum E { M(String) }Mserialize_newtype_variant
变长序列seqVec<T>, HashSet<T>serialize_seq
定长元组tuple(A, B, C)serialize_tuple
元组结构tuple_structstruct Pair(i32, i32);serialize_tuple_struct
元组变体tuple_variantenum E { V(A, B) }Vserialize_tuple_variant
键值映射mapHashMap<K, V>serialize_map
结构体structstruct User { ... }serialize_struct
结构变体struct_variantenum E { V { a: A } }Vserialize_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_noneserialize_someoption 这一个原语下的两个调用,而不是两个独立原语?因为它们在语义上表达的是"存在/不存在"这个同一个概念的两种取值。同样的逻辑也适用于后面会看到的 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 的 u8i64 在 JSON 文本里写出来没区别,但在 Bincode 里一个占 1 字节、一个占 8 字节。Serde 把 14 种数值都拆开,格式实现者可以针对每种类型写最紧凑的编码。

原因二:避免精度损失。 Rust 有 f32f64 两种浮点。如果只给 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&lt;u8&gt;? 这是一个让无数人困惑的设计。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] 自动优化。

设计意图bytesseq 分开,是 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_variantNone)+ newtype_variantSome(T))表达。

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

不同格式的 option 处理:

格式None 编码Some(42) 编码
JSONnull42
MessagePack0xc0 (nil)0x2a (整数 42)
Bincode0x00 (1 字节 tag)0x01 <42 的 8 字节>
Postcard0x000x01 <42 的 varint>

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

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

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_variantname(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);

在类型系统里,UserIdu64 是不同类型,编译器不会让你把 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_structname("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 } 的字段永远是 idname

这个区别决定了:

  • 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 编码每个字段都要写字段名(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。格式只是被动执行。
  • 反序列化方向:是类型提示格式、但格式决定VisitorVec<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 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、FlexBuffers with schema。这类格式的 deserialize_any 通常直接返回错误——因为解析器看到一串字节,没有 schema 就不知道这是个 u32 还是个 string。

实际影响:serde_json::Valuetoml::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::IpAddrSocketAddrDuration 这类类型在人类可读格式下序列化成字符串("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_anyis_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 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 的真实用户——9 处 IP/网络类型

§2.11.1 标题"is_human_readable——一个 bool 撑起的格式分野"——把这个 bool 的真实使用场景在源码里实测一次——

grep is_human_readable serde_core/src/{ser,de}/impls.rs10 处使用——全部集中在两类:

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

Rust 类型序列化分支(human_readable=true)二进制分支
IpAddrdispatch 到 Ipv4Addr / Ipv6Addrdispatch 到对应二进制路径
Ipv4Addr"192.0.2.1" 字符串(最长 15 字符)4 字节大端
Ipv6Addr"2001:db8::1" 字符串(最长 39 字符)16 字节
SocketAddrdispatch 到 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
}

两条值得记住的物理事实——

  1. is_human_readable 在 std 类型里仅 9 处使用——全是网络地址类型的双格式分支——Serde 没把这个 bool 撒到处都用——印证 §2.11.1 "一个 bool 撑起的格式分野" 的"克制"含义:不是每个类型都需要双路径,只在压缩收益显著的网络地址类型上做差别——这是"优化要在数据点上选择"的工程纪律
  2. debug_assert_eq!(MAX_LEN, "101.102.103.104".len())——Ipv4Addr 序列化代码里硬编码 15 字符上限作为栈缓冲区大小、用 debug 断言验证不会溢出——是 §2.11 "Serde 在性能上的苛刻" 的具体例:避免堆分配、用栈数组 + 边界证明——和 §3.10.1 测得的 tower-layer/tuple.rs 330 行手展 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:

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

动手实验

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

延伸阅读

基于 VitePress 构建