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 原样打出来。
货主(类型)不管货最后上哪条船,只按标准把货装进几种规格的集装箱(数据模型的 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)汇总在设计取舍。
问: 让每个类型实现 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 方法。
| 数据模型 | 例子 | Serializer | Deserializer 提示 | Visitor 回调 |
|---|---|---|---|---|
| bool | bool | serialize_bool | deserialize_bool | visit_bool |
| i8 | i8 | serialize_i8 | deserialize_i8 | visit_i8(默认转 visit_i64) |
| i16 | i16 | serialize_i16 | deserialize_i16 | visit_i16(默认转 visit_i64) |
| i32 | i32 | serialize_i32 | deserialize_i32 | visit_i32(默认转 visit_i64) |
| i64 | i64 | serialize_i64 | deserialize_i64 | visit_i64 |
| i128 | i128 | serialize_i128(默认报错) | deserialize_i128(默认报错) | visit_i128 |
| u8 | u8 | serialize_u8 | deserialize_u8 | visit_u8(默认转 visit_u64) |
| u16 | u16 | serialize_u16 | deserialize_u16 | visit_u16(默认转 visit_u64) |
| u32 | u32 | serialize_u32 | deserialize_u32 | visit_u32(默认转 visit_u64) |
| u64 | u64 | serialize_u64 | deserialize_u64 | visit_u64 |
| u128 | u128 | serialize_u128(默认报错) | deserialize_u128(默认报错) | visit_u128 |
| f32 | f32 | serialize_f32 | deserialize_f32 | visit_f32(默认转 visit_f64) |
| f64 | f64 | serialize_f64 | deserialize_f64 | visit_f64 |
| char | char | serialize_char | deserialize_char | visit_char(默认转 visit_str) |
| string | str、String | serialize_str | deserialize_str、deserialize_string | visit_str、visit_borrowed_str、visit_string |
| byte array | 显式调 serialize_bytes 的类型([u8] 本身按 seq 走) | serialize_bytes | deserialize_bytes、deserialize_byte_buf | visit_bytes、visit_borrowed_bytes、visit_byte_buf |
| option | Option<T> | serialize_none、serialize_some | deserialize_option | visit_none、visit_some |
| unit | () | serialize_unit | deserialize_unit | visit_unit |
| unit_struct | struct Unit; | serialize_unit_struct | deserialize_unit_struct | visit_unit |
| unit_variant | E::A | serialize_unit_variant | deserialize_enum | visit_enum,再 unit_variant |
| newtype_struct | struct Mm(u8) | serialize_newtype_struct | deserialize_newtype_struct | visit_newtype_struct |
| newtype_variant | E::N(u8) | serialize_newtype_variant | deserialize_enum | visit_enum,再 newtype_variant_seed |
| seq | Vec<T>、HashSet<T> | serialize_seq | deserialize_seq | visit_seq |
| tuple | (u8, String)、[u64; 10] | serialize_tuple | deserialize_tuple | visit_seq |
| tuple_struct | struct Rgb(u8, u8, u8) | serialize_tuple_struct | deserialize_tuple_struct | visit_seq |
| tuple_variant | E::T(u8, u8) | serialize_tuple_variant | deserialize_enum | visit_enum,再 tuple_variant → visit_seq |
| map | BTreeMap<K, V> | serialize_map | deserialize_map | visit_map |
| struct | struct S { r: u8 } | serialize_struct | deserialize_struct | visit_map(也可以是 visit_seq) |
| struct_variant | E::S { r: u8 } | serialize_struct_variant | deserialize_enum | visit_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 的主题。
表里只有 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 的文档在列完数据模型之后,紧接着说它支持两种入口:
- The
deserialize_anymethod. Self-describing data formats like JSON are able to look at the serialized data and tell what it represents. ...- 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。
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()));
格式不一定是字节。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。
| serde | borsh 1.8.1 | rkyv 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, _> |
| 反序列化 | 格式驱动,回调 Visitor | fn 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 种。方法数不一一相等:Serializer30 个(option 拆两个),Deserializer31 个(借用 / 拥有两套提示、四种变体合成deserialize_enum、外加any/identifier/ignored_any),Visitor27 个(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」的默认实现。