Rust in Everything
基于 serde 1.0.229

serde 解决什么问题

主线位置:三条主线开始之前。序列化主线里类型把数据「推」给 Serializer,反序列化主线里格式通过 Visitor「回调」类型,两条线中间隔着的都是本页讲的数据模型。下一页「一次往返」拿一个具体的 struct Post 把两条线各走一遍。

flowchart TB
  post[struct Post] --> ser["Serialize / Deserialize"]
  vec["Vec T、String"] --> ser
  other[其他类型] --> ser
  ser --> model["数据模型:29 种类型"]
  model --> fmt["Serializer / Deserializer"]
  fmt --> json[serde_json]
  fmt --> bin[bincode]
  fmt --> pc[postcard]
  fmt --> yaml[serde_yaml]

M × N 变成 M + N

假设有 M 个类型要存成 N 种格式。最直接的写法是每一对写一份代码:Post 转 JSON、Post 转 YAML、User 转 JSON……一共 M × N 份。serde 在中间插了一层数据模型:类型只负责把自己说成数据模型里的若干种值(实现 Serialize / Deserialize),格式只负责把数据模型里的值写成字节或从字节读出来(实现 Serializer / Deserializer)。双方互不认识,代码量是 M + N。

下面这个程序只给 Post 写了两行 #[derive],同一个值写进四种格式,再从四种格式读回来。类型定义:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Post {
  pub title: String,
  pub tags: Vec<String>,
  pub status: Status,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub enum Status {
  Draft,
  Published(u32),
}

src/bin/formats.rs(依赖锁在 serde =1.0.229、serde_json =1.0.154、bincode =1.3.3、postcard 1(锁到 1.1.3)、serde_yaml =0.9.34):

use exp_serde_overview::model::{Post, Status};

fn main() {
  let post = Post {
    title: "serde".to_string(),
    tags: vec!["rust".to_string(), "json".to_string()],
    status: Status::Published(2026),
  };

  // One derive, several formats.
  let json = serde_json::to_string(&post).unwrap();
  let yaml = serde_yaml::to_string(&post).unwrap();
  let bin = bincode::serialize(&post).unwrap();
  let pc = postcard::to_allocvec(&post).unwrap();
  println!("json     {} bytes: {json}", json.len());
  println!("yaml     {} bytes: {yaml:?}", yaml.len());
  println!("bincode  {} bytes: {bin:?}", bin.len());
  println!("postcard {} bytes: {pc:?}", pc.len());

  // Each one reads back into the same type.
  let a: Post = serde_json::from_str(&json).unwrap();
  let b: Post = serde_yaml::from_str(&yaml).unwrap();
  let c: Post = bincode::deserialize(&bin).unwrap();
  let d: Post = postcard::from_bytes(&pc).unwrap();
  println!("all equal: {}", [a, b, c, d].iter().all(|p| *p == post));

  // A self-describing target type needs deserialize_any.
  // ...(剩下几行见本页的动脑筋)
}

M2 Max、macOS、rustc 1.99.0,cargo run --bin formats 的前五行:

json     68 bytes: {"title":"serde","tags":["rust","json"],"status":{"Published":2026}}
yaml     57 bytes: "title: serde\ntags:\n- rust\n- json\nstatus: !Published 2026\n"
bincode  53 bytes: [5, 0, 0, 0, 0, 0, 0, 0, 115, 101, 114, 100, 101, 2, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 114, 117, 115, 116, 4, 0, 0, 0, 0, 0, 0, 0, 106, 115, 111, 110, 1, 0, 0, 0, 234, 7, 0, 0]
postcard 20 bytes: [5, 115, 101, 114, 100, 101, 2, 4, 114, 117, 115, 116, 4, 106, 115, 111, 110, 1, 234, 15]
all equal: true

Post 不知道这四种格式的存在,四个格式 crate 也不知道 Post。它们之间只交换了一串数据模型里的调用:「一个叫 Post 的 struct,3 个字段;字段 title 是一个 string……」。这串调用具体是什么,下一页用一个打印调用的 Serializer 原样打出来。

💡TIP
打个比方:标准集装箱

货主(类型)不管货最后上哪条船,只按标准把货装进几种规格的集装箱(数据模型的 29 种类型):散件装 u32 箱,成排的装 seq 箱,有名有姓的一组货装 struct 箱,箱上贴好标签("Post"、字段名、变体名和序号)。船公司(格式)只认箱子不认货:JSON 这条船把标签上的字段名印在箱子外面,bincode 这条船只看箱子的顺序,标签上的名字一个也不印。

比方到这里为止:真集装箱是先装好再交给船的,serde 没有「装好的箱子」这个中间产物,Serialize 每说一句,Serializer 当场就写出去了。卸货时也不是货主去开箱,而是船(格式)一件一件递给货主派来的人(Visitor)。

为什么是 trait,而不是反射或 Value

Rust 没有运行时反射,程序跑起来以后没法问一个值「你有哪些字段」。serde 的做法是在编译期把这些信息写成代码:#[derive(Serialize)] 读结构体定义,生成一个逐字段调用 Serializer 的 impl(派生宏的流水线讲它怎么生成)。

格式那一侧是一个泛型参数。Serialize 只有一个方法:

pub trait Serialize {
    // ...
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer;
}

S 在编译期确定,Post::serialize::<&mut serde_json::Serializer<Vec<u8>>> 和 Post::serialize::<&mut bincode::Serializer<..>> 是两份独立的机器码,每次调用都可以内联,中间没有 trait 对象,也没有一棵中间的树。这个选择的代价(编译时间、代码体积、Serializer 不能做成 dyn)汇总在设计取舍。

📝NOTE
没有蠢问题

问: 让每个类型实现 Into<serde_json::Value>,格式再去读 Value,不也是 M + N 吗?

答: 数量上是,但多出两个代价。第一,Value 是一棵真的树,每个字符串、数组、对象都要单独分配一次,写完还要再遍历一遍。第二,Value 是 JSON 的模型,它只有 null、bool、数字、字符串、数组、对象六种,struct 和 map 的区别、变体序号、Post 这个名字都没地方放。把上面的 Post 先 serde_json::to_value 再交给 bincode:

direct 53 bytes, via Value 125 bytes
first 16 bytes via Value: [3, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0]
read back: io error:  / Io(Custom { kind: UnexpectedEof, error: "" })

绕道之后 bincode 收到的是一个 map:先写长度 3,再写第一个键的长度 6,也就是按 BTreeMap 排序后排第一的 "status";字段名全被写进了字节,体积从 53 涨到 125。按 Post 读回时,bincode 按字段顺序读第一个字段 title,把 3 当成字符串长度、把后面的 [6, 0, 0] 当成内容,之后全部错位,最后读到末尾报 UnexpectedEof。数据模型保留了 struct、map、变体序号的区别,每个格式自己决定用不用,Value 在第一步就把它们抹掉了。

数据模型:29 种类型

数据模型的清单写在 Serializer 的文档里,Deserializer 的文档里一字不差地又抄了一遍。原文说每个 Rust 数据结构都归入 「one of 29 possible types」,数一下:14 种基本类型(bool、5 种有符号整数、5 种无符号整数、2 种浮点数、char),加上 string、byte array、option、unit、unit_struct、unit_variant、newtype_struct、newtype_variant、seq、tuple、tuple_struct、tuple_variant、map、struct、struct_variant 15 种,正好 29。

下表是每种类型在三个 trait 上对应的方法。Serializer 一列是 Serializer 的方法;「Deserializer 提示」一列是 Deserializer 的方法,由目标类型调用,告诉格式「我想要什么」;「Visitor 回调」一列是格式读到数据后调用的 Visitor 方法。

数据模型例子SerializerDeserializer 提示Visitor 回调
boolboolserialize_booldeserialize_boolvisit_bool
i8i8serialize_i8deserialize_i8visit_i8(默认转 visit_i64)
i16i16serialize_i16deserialize_i16visit_i16(默认转 visit_i64)
i32i32serialize_i32deserialize_i32visit_i32(默认转 visit_i64)
i64i64serialize_i64deserialize_i64visit_i64
i128i128serialize_i128(默认报错)deserialize_i128(默认报错)visit_i128
u8u8serialize_u8deserialize_u8visit_u8(默认转 visit_u64)
u16u16serialize_u16deserialize_u16visit_u16(默认转 visit_u64)
u32u32serialize_u32deserialize_u32visit_u32(默认转 visit_u64)
u64u64serialize_u64deserialize_u64visit_u64
u128u128serialize_u128(默认报错)deserialize_u128(默认报错)visit_u128
f32f32serialize_f32deserialize_f32visit_f32(默认转 visit_f64)
f64f64serialize_f64deserialize_f64visit_f64
charcharserialize_chardeserialize_charvisit_char(默认转 visit_str)
stringstr、Stringserialize_strdeserialize_str、deserialize_stringvisit_str、visit_borrowed_str、visit_string
byte array显式调 serialize_bytes 的类型([u8] 本身按 seq 走)serialize_bytesdeserialize_bytes、deserialize_byte_bufvisit_bytes、visit_borrowed_bytes、visit_byte_buf
optionOption<T>serialize_none、serialize_somedeserialize_optionvisit_none、visit_some
unit()serialize_unitdeserialize_unitvisit_unit
unit_structstruct Unit;serialize_unit_structdeserialize_unit_structvisit_unit
unit_variantE::Aserialize_unit_variantdeserialize_enumvisit_enum,再 unit_variant
newtype_structstruct Mm(u8)serialize_newtype_structdeserialize_newtype_structvisit_newtype_struct
newtype_variantE::N(u8)serialize_newtype_variantdeserialize_enumvisit_enum,再 newtype_variant_seed
seqVec<T>、HashSet<T>serialize_seqdeserialize_seqvisit_seq
tuple(u8, String)、[u64; 10]serialize_tupledeserialize_tuplevisit_seq
tuple_structstruct Rgb(u8, u8, u8)serialize_tuple_structdeserialize_tuple_structvisit_seq
tuple_variantE::T(u8, u8)serialize_tuple_variantdeserialize_enumvisit_enum,再 tuple_variant → visit_seq
mapBTreeMap<K, V>serialize_mapdeserialize_mapvisit_map
structstruct S { r: u8 }serialize_structdeserialize_structvisit_map(也可以是 visit_seq)
struct_variantE::S { r: u8 }serialize_struct_variantdeserialize_enumvisit_enum,再 struct_variant → visit_map

「unit_variant」「newtype_variant_seed」「tuple_variant」「struct_variant」是 VariantAccess 的方法,在 SeqAccess、MapAccess 与 EnumAccess 里讲。从表里能读出三件事。

三个 trait 的方法数并不都是 29。 Serializer 有 30 个 serialize_*,因为 option 拆成了 serialize_none 和 serialize_some。Deserializer 有 31 个 deserialize_*:string 和 byte array 各有一对借用 / 拥有的提示,四种变体合成一个 deserialize_enum(变体是哪种要等读到数据才知道,在 VariantAccess 那一步才分开),再加上三个不对应任何类型的入口:deserialize_any(「你看着办」)、deserialize_identifier(「下一个是字段名或变体名」)、deserialize_ignored_any(「这个值我不要,跳过它」)。Visitor 有 27 个 visit_*,没有 visit_tuple 和 visit_struct:tuple、tuple_struct 都以 visit_seq 交付,struct 以 visit_map 交付。也正因为这样,serde_json 的 deserialize_struct 读到 [ 时可以照样把一个数组交给 struct 的 visit_seq(serde_json 的 Deserializer)。

数据模型带的信息比任何一种格式需要的都多。 serialize_unit_variant(name, variant_index, variant) 同时给出类型名、变体序号和变体名,serialize_struct(name, len) 给出类型名和字段数。每个格式只挑自己要的:上面的输出里,JSON 写了变体名 "Published"、没写 "Post";bincode 只写了变体序号(1, 0, 0, 0 是 u32 的 1)和每个字符串的 u64 长度,字段名一个都没写;postcard 把同样的东西写成变长整数(234, 15 是 2026)。serde_json 的 serialize_struct 只用 name 认几个魔法名字(arbitrary_precision、raw_value 开启时),其余一律转给 serialize_map。

提示和回调不必一一对应。 目标是 u32 时,类型调 deserialize_u32,但 JSON 里的数字不分 u32 和 u64,serde_json 回调的是 visit_u64,再由 u32 的 visitor 自己检查范围(下一页的日志里能看到)。Deserializer 的方法只是提示,Visitor 那边要准备好接收几种可能,这是 Deserialize、Deserializer 与 Visitor 的主题。

📝NOTE
冷知识:i128 和 u128 是后加的,所以只有它们带默认实现

表里只有 serialize_i128 / serialize_u128 和 deserialize_i128 / deserialize_u128 在 trait 里有默认实现,其余基本类型的方法都是必须实现的。默认实现就是报错:

    fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
        let _ = v;
        Err(Error::custom("i128 is not supported"))
    }

这两种整数是 #1263(「Add Serde impls for i128 and u128」,修 #1136「i128 and u128 integers missing Deserialize impls」)加进来的,首次发布在 1.0.60,release notes 写的是「Add Serde impls for i128 and u128 when compiling with Rust 1.26+ (#1263)」。之前数据模型里没有 128 位整数;加进来的那个提交(「Add integer128 to Serde traits」)给新方法带上了上面这种报错的默认实现。这时 1.0 已经发布一年多(v1.0.0 是 2017-04-20),带默认实现的新方法不会让任何已有的 Serializer / Deserializer 编译失败;代价是不支持 128 位的格式在运行期才报 i128 is not supported。Visitor 那一侧的 visit_i128 也有默认实现,和其他 visit_* 一样报 invalid_type。

自描述格式与非自描述格式

Deserializer 的文档在列完数据模型之后,紧接着说它支持两种入口:

  1. The deserialize_any method. Self-describing data formats like JSON are able to look at the serialized data and tell what it represents. ...
  2. The various deserialize_* methods. Non-self-describing formats like Postcard need to be told what is in the input in order to deserialize it.

对照上面 bincode 的 53 个字节就能看出区别。JSON 的输入自带类型:{ 后面一定是对象," 后面一定是字符串,格式看一眼就能决定调哪个 visit_*,所以它能实现 deserialize_any。bincode 的输入只是一串没有标记的数:开头的 5, 0, 0, 0, 0, 0, 0, 0 可以是一个 u64 的 5,也可以是字符串长度,也可以是 8 个 u8。只有类型通过 deserialize_string、deserialize_seq 告诉它「下面是什么」,它才知道怎么读。

于是有一类类型只能配自描述格式:它们自己也不知道下面是什么,只能调 deserialize_any。serde_json::Value 是典型(它的 Deserialize 只调 deserialize_any,见 Value),#[serde(untagged)]、内部标签、flatten 生成的代码也是(缓冲主线,见 Content)。文档对实现 Deserialize 的人的建议是:除非真需要格式告诉你输入里是什么,否则不要依赖 deserialize_any。

❗IMPORTANT
动脑筋:同一个 Post 用 bincode 和 postcard 序列化之后,再分别读成 serde_json::Value,会发生什么?

平台 M2 Max、macOS、rustc 1.99.0。接着上面 formats.rs 的最后三段:

  let v: serde_json::Value = serde_json::from_str(&json).unwrap();
  println!("json -> Value: {v}");
  let v: Result<serde_json::Value, _> = bincode::deserialize(&bin);
  println!("bincode -> Value: {:?}", v.map_err(|e| e.to_string()));
  let v: Result<serde_json::Value, _> = postcard::from_bytes(&pc);
  println!("postcard -> Value: {:?}", v.map_err(|e| e.to_string()));
三行各打印什么?JSON 那一行里键的顺序和原来一样吗?

格式不一定是字节。serde_json::value::Serializer 的 Ok 类型是 Value,serde_json::to_value 就是 value.serialize(Serializer),输出是一棵内存里的树。axum 的 Path 提取器自己写了一个 PathDeserializer,输入是路由匹配出来的参数列表(Path 与 Query)。数据模型两侧只约定调用,不约定介质。

和别的做法比一比

只比设计,不比速度,也只比能对着源码核对的两个库:本机 cargo 缓存里的 rkyv 0.7.46 和 borsh 1.8.1。

serdeborsh 1.8.1rkyv 0.7.46
中间层29 种类型的数据模型没有,类型直接写字节没有,类型定义自己的归档形式
序列化签名fn serialize<S: Serializer>(&self, serializer: S)fn serialize<W: Write>(&self, writer: &mut W)Serialize<S>::serialize(&self, serializer: &mut S) -> Result<Self::Resolver, _>
反序列化格式驱动,回调 Visitorfn deserialize_reader<R: Read>(reader: &mut R) -> Result<Self>,类型自己读Deserialize<T, D> 的 self 是归档后的值;不反序列化也能直接读
一个 derive 对应几种格式任意多种一种(borsh 字节格式)一种(rkyv 归档格式)

borsh 的 trait 直接拿 io::Write / io::Read,格式就是 trait 本身,换格式等于换一套 trait。rkyv 的 Archive 有一个关联类型 Archived,即「这个类型在字节里长什么样」;crate 文档写的是 「Unlike serde, rkyv produces data that is guaranteed deserialization free」,把文件 mmap 进来、转一下指针就能用。两者都放弃了「一个类型配任意格式」,换来的是格式和类型之间没有间接层。serde 的选择正好反过来:留下数据模型这层间接,换取 M + N。

动脑筋答案

json -> Value: {"status":{"Published":2026},"tags":["rust","json"],"title":"serde"}
bincode -> Value: Err("Bincode does not support the serde::Deserializer::deserialize_any method")
postcard -> Value: Err("This is a feature that PostCard will never implement")
  • JSON 读成 Value 成功,但键的顺序变成了 status、tags、title:Value::Object 里的 Map 默认是 BTreeMap,按键排序;开 preserve_order 才换成保序的 IndexMap(Value)。
  • bincode 和 postcard 都失败了,失败在 deserialize_any 上:Value 不知道下一个值是什么,只能请格式「看着办」,而这两种格式的字节里没有类型标记,没法看。bincode 的报错直接点名了这个方法;postcard 的报错是一句「This is a feature that PostCard will never implement」。
  • 读回 Post 时这两种格式都没问题(前面 all equal: true),因为 Post 的 derive 只调具体的提示:deserialize_struct、deserialize_string、deserialize_seq、deserialize_enum、deserialize_u32。

小结

  • serde 在类型和格式之间放了一个数据模型:类型实现 Serialize / Deserialize,格式实现 Serializer / Deserializer,M × N 变成 M + N。格式不一定是字节,serde_json::Value 和 axum 的 PathDeserializer 都是。
  • 数据模型按 Serializer 文档的清单数是 29 种:14 种基本类型加 15 种。方法数不一一相等:Serializer 30 个(option 拆两个),Deserializer 31 个(借用 / 拥有两套提示、四种变体合成 deserialize_enum、外加 any / identifier / ignored_any),Visitor 27 个(tuple 走 visit_seq、struct 走 visit_map)。
  • 数据模型带全了类型名、字段名、变体名和序号,格式按需取用:JSON 写名字,bincode 和 postcard 写序号和长度。
  • 不用反射,因为 Rust 没有;不用 Value 做中间层,因为它多一次分配,还把 struct、map 和变体序号的区别抹掉了。实测绕道 Value 后的 bincode 字节从 53 涨到 125,且读不回 Post。
  • 自描述格式(JSON)能实现 deserialize_any;非自描述格式(bincode、postcard)只能按类型给的提示读,读 serde_json::Value 会在 deserialize_any 上失败。
  • i128 / u128 在 1.0.60(#1263)才进数据模型,所以只有它们的 serialize_* / deserialize_* 带「i128 is not supported」的默认实现。

💬 关联讨论

发起讨论
加载中...