Serde 元编程

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

作者 杨艺韬 · 8,279 字

第 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 就是这种中间语。它要同时满足两个矛盾的约束:

这两个约束把 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_noneserialize_someoption 这一个原语下的两个调用,而不是两个独立原语?因为它们在语义上表达的是"存在/不存在"这个同一个概念的两种取值。同样的逻辑也适用于后面会看到的 seq 状态机(用 serialize_seq + 多次 serialize_element + end)——一个原语对应一种语义抽象,而不是一个方法对应一种抽象。

Serializer 的真实 trait 定义见 serde/serde_core/src/ser/mod.rs:355

// 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 位的格式可以明确拒绝:

// 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 里,差别巨大:

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,通过两个方法表达:

// 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) 编码
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 设计——bytesoptionunit 都是同样的原因。

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

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

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

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

// 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>;

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

如果 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 包装一个原始类型,获得类型安全和新语义:

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

在类型系统里,UserIdu64 是不同类型,编译器不会让你把 i 当作 UserId 传递。但在运行时,UserId 的内存布局和 u64 完全一样——repr(transparent)

Serde 给 newtype 一个专门的原语:

// 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/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 版本的同样思路:

// 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 种"元素列表"原语:

它们的 Serializer 方法签名:

// 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 字节。

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

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

状态机:为什么 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()

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

如果 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 种键值映射

最后三个原语:

它们的方法签名:

// 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 是什么时候决定的?

这个区别决定了:

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 形态:

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 个方法如何把 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>;
    // ... 对应 29 种原语各一个方法,加上 deserialize_any 一共 30+ 个

    fn is_human_readable(&self) -> bool { true }   // line 1253
}

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

所以 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> { ... }
    // ... 对应 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):

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 强制实现。这个设计让你可以"只接受一种原语":

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):

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:

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

MessagePack(十六进制):

82 a2 69 64 2a a4 6e 61 6d 65 a5 61 6c 69 63 65

Bincode:

2a 00 00 00 00 00 00 00 05 00 00 00 00 00 00 00 61 6c 69 63 65

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

Postcard(类似 Bincode 但用 varint):

2a 05 61 6c 69 63 65

同一份数据,文本格式占 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) 二进制分支
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 里的真实代码——

// 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 种原语不是随意选的,它们对应:

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

  1. 类型信息不丢失:14 种数值分别对应,名字通过参数传递,格式可选择性利用。
  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_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 字段的区分成为一个问题?

延伸阅读