serde 源码解析
全卷只对着一个版本写:
| crate | 版本 | tag | commit | 日期 |
|---|---|---|---|---|
| serde | 1.0.229 | v1.0.229 | 7fc3b4c | 2026-07-18 |
| serde_json | 1.0.154 | v1.0.154 | 20572a6 | 2026-10-11 |
serde 仓库是一个工作区:serde、serde_core、serde_derive 三个 crate 同版本、同 tag 发布,serde_derive_internals 也在这个工作区里,但有自己的版本号(Cargo.toml 写的是 0.29.1)。serde_json 在另一个仓库,这一卷拿它当「一个格式」的例子。四个 crate 各管什么、谁依赖谁,见源码地图。
serde 要解决的是「M 个类型 × N 种格式」:每个类型要能写成 JSON、YAML、bincode……,每种格式要能读写每个类型。serde 在中间放了一个固定的数据模型,Serializer 的文档写的是「categorize every Rust data structure into one of 29 possible types」(ser/mod.rs)。类型实现 Serialize / Deserialize,只跟数据模型说话;格式实现 Serializer / Deserializer,也只跟数据模型说话。两边都不知道对方是谁,组合数从 M × N 变成 M + N。
这一切都是编译期的:没有运行时反射,#[derive(Serialize, Deserialize)] 是一个过程宏,在编译时为每个类型生成两个 impl;serde_json::to_string::<Post> 被单态化成一份专门处理 Post 的代码。读这一卷,读的就是这两样东西:serde_core 里那几个 trait 怎么分工,以及 serde_derive 为一个 struct 或 enum 生成了什么。axum 卷的 body 提取器和 Path、Query 与请求元数据讲 axum 怎么调 serde_json 和 serde_urlencoded;这一卷讲被调的那一侧。
全卷沿三条线展开:
- 序列化主线(一次
to_string):derive 生成的Serialize::serialize调serialize_struct("Post", 2),对每个字段serialize_field,最后end。serde_json 的Serializer在这三步里分别写{、键值对、},逗号和缩进交给Formatter。数据从类型推给格式。 - 反序列化主线(一次
from_str):derive 生成的Deserialize::deserialize调deserialize_struct("Post", FIELDS, __Visitor);serde_json 看到{,回调__Visitor::visit_map,后者循环next_key/next_value,缺字段走missing_field。格式驱动,类型通过Visitor被回调:控制权是反过来的。'de让&str字段直接借用输入。 - 缓冲主线(untagged、内部标签、
flatten):读到决定性的信息之前,不知道该用哪个Visitor。生成的代码先把整段输入读成一棵Content树,再对这棵树重放反序列化。代价是多一次分配和遍历、丢掉格式的类型提示,以及只剩一句话的报错。
三条线每一步对应的源码和页面,见怎么读这一卷。
flowchart TB intro[00 导读] --> overview[01 全景] overview --> core[02 核心 trait] core --> derive[03 派生宏] derive --> enums["04 enum 与缓冲"] core --> fmt["05 serde_json"] enums --> design[06 设计取舍] fmt --> design design --> pit[07 常见坑] pit --> site[08 在本站中] site --> hands[09 动手]
实验环境
关于行为的说法(调了哪个方法、按什么顺序、报什么错、写出什么 JSON、rustc 说了什么)都配一个小程序和它的真实输出,平台是 M2 Max、macOS、rustc 1.99.0。serde 没有平台相关的代码路径,只在 macOS 上跑。看 derive 生成的代码用 cargo expand,引用时只摘关键几行。
各部分
- 00 导读:锁定的版本,本站锁的 1.0.228 / 1.0.150 到本卷版本的每一处改动;三条主线、阅读路线、怎么看生成代码;工作区四个 crate 的依赖关系、feature、
__private229这个带补丁号的模块名、test_suite/的组织。 - 01 全景:数据模型为什么能把 M × N 变成 M + N,自描述格式和非自描述格式;一次
to_string加一次from_str的完整往返,用打印调用的手写Serializer和Visitor对照生成代码。 - 02 核心 trait:本卷重点。
Serialize与Serializer,Deserialize、Deserializer与Visitor,SeqAccess/MapAccess/EnumAccess,生命周期'de,错误,标准库类型的实现,de::value与IntoDeserializer。 - 03 派生宏:从
syn::DeriveInput到const _: () = { ... }的流水线,属性,生成的Serialize、生成的Deserialize,泛型与 bound。 - 04 enum 的表示与缓冲:外部标签、内部标签与相邻标签、untagged,
Content和它丢掉的东西,flatten。 - 05 一个格式:serde_json:序列化一侧的
Compound与Formatter,反序列化一侧的Read与借用,Value。 - 06 设计取舍:把各章的取舍汇总到一起,每条链回有证据的页面。
- 07 常见坑:用报错原文做标题,每个都先跑出最小复现。
- 08 在本站中:本站的配置结构体、LLM 客户端的标签 enum、前置元数据怎么用 serde;1.0.228 → 1.0.229 和 serde_json 1.0.150 → 1.0.154 的改动是否影响本站。
- 09 动手:不依赖 serde,从零写一个迷你 serde 和一个最小的
#[derive(Serialize)]。
从「导读」开始。只想会用 serde 的读者,读 01 全景、03 里的属性、04 enum 的表示和常见坑;想读懂实现的,按顺序读。02 是全卷的重点:三条主线都从那里的 trait 出发,03 和 05 分别从类型一侧和格式一侧往下走。