Appearance
第 17 章 serde_json 源码:手写解析器如何接入 Data Model
17.1 从另一个角度看 Serde
前面 16 章都从 serde 本身的视角讲——Data Model、trait 设计、derive 宏如何生成代码。本章换个角度:一个具体格式(JSON)如何实现 Serializer 和 Deserializer?
serde_json 是 Serde 生态最知名的格式 crate——几乎所有 Rust Web 服务都在用它。它的核心入口文件约 10,300 行 Rust(本书版本 1.0.149,wc -l src/*.rs src/io/*.rs 实测),完整 src/ 加上 lexical/ 等子模块见 §17.12.1 的尺寸表。本章不逐行讲这些源码,而是聚焦一个核心问题:
一个格式实现者如何把自己"接入" Serde?
回答这个问题需要看三部分源码:
serde_json::Serializer(ser.rs,2285 行):如何实现 Serializer traitserde_json::Deserializer(de.rs,2704 行):如何实现 Deserializer traitserde_json::Value(value/ 目录):如何用 enum 表示任意 JSON
读完本章你会建立一个具体参照系——"当我要为我的格式写 Serde 支持时,代码应该长这样"。
本书基于 serde_json 1.0.149(commit dc8003a)。
17.2 源码结构
先看文件组织:
serde_json/src/
├── lib.rs 441 行 入口 + re-export
├── ser.rs 2285 行 Serializer + Formatter + to_string/to_writer 等
├── de.rs 2704 行 Deserializer + 所有 visit 路径
├── read.rs 1089 行 Read trait + SliceRead/StrRead/IoRead
├── value/ 子目录 Value enum + 它的 Serialize/Deserialize
│ ├── mod.rs Value 定义
│ ├── de.rs Value 的 Deserialize 实现
│ ├── ser.rs Value 的 Serialize 实现
│ └── ...
├── map.rs 1181 行 Map<String, Value>(保持插入顺序的 HashMap)
├── number.rs 814 行 Number 类型(i64/u64/f64 统一表示)
├── error.rs 541 行 Error 类型
├── raw.rs 784 行 RawValue(不解析的 JSON 片段)
├── macros.rs 303 行 json! 宏
├── iter.rs 70 行 Deserializer::into_iter
└── lexical/ 子目录 浮点数格式化库三大支柱:
ser.rs和de.rs是对外接口——实现 Serde trait,让任何数据结构能 to/from JSON。value/是动态树结构——如果你不知道 JSON 长什么样,可以先解析成 Value,运行时探索。read.rs是输入抽象——无论从 &str、&[u8]、io::Read 都可以统一接口。
本章聚焦前两个。read.rs 作为支持部分会略微提到。
17.3 入口函数:from_str 和 to_string
最常用的两个函数(serde_json/src/de.rs:2699 和 ser.rs:2245):
rust
// 简化版
pub fn from_str<'a, T>(s: &'a str) -> Result<T>
where T: de::Deserialize<'a>,
{
from_trait(read::StrRead::new(s))
}
pub fn to_string<T>(value: &T) -> Result<String>
where T: ?Sized + Serialize,
{
let mut writer = Vec::with_capacity(128);
to_writer(&mut writer, value)?;
// SAFETY: writer 内容总是有效 UTF-8
let string = unsafe { String::from_utf8_unchecked(writer) };
Ok(string)
}from_str 用 StrRead 包装 &'a str('a 就是第 15 章的 'de),然后调用通用 from_trait 函数做实际反序列化。
to_string 先写入 Vec<u8>,然后 unsafe 转 String——因为 serde_json 保证输出总是 valid UTF-8(所有字符串字段都是 str,非字符串字段都是 ASCII)。这个 unsafe 优化省掉 UTF-8 验证(否则 from_utf8 要遍历整个字节数组)。
17.4 Serializer 实现:2285 行做什么
serde_json::Serializer 的 trait 实现是本章的核心之一。看它的结构(serde_json/src/ser.rs:17):
rust
pub struct Serializer<W, F = CompactFormatter> {
writer: W,
formatter: F,
}
impl<W: io::Write, F: Formatter> ser::Serializer for &'a mut Serializer<W, F> {
type Ok = ();
type Error = Error;
type SerializeSeq = Compound<'a, W, F>;
type SerializeTuple = Compound<'a, W, F>;
type SerializeTupleStruct = Compound<'a, W, F>;
type SerializeTupleVariant = Compound<'a, W, F>;
type SerializeMap = Compound<'a, W, F>;
type SerializeStruct = Compound<'a, W, F>;
type SerializeStructVariant = Compound<'a, W, F>;
// ↑ 所有 7 个关联类型都是 Compound——复用同一个状态机类型
fn serialize_bool(self, value: bool) -> Result<()> {
self.formatter.write_bool(&mut self.writer, value).map_err(Error::io)
}
fn serialize_i8(self, value: i8) -> Result<()> {
self.formatter.write_i8(&mut self.writer, value).map_err(Error::io)
}
// ... 共 30 个方法,每个都是"调 formatter 的对应方法"
}三个设计要点:
1. 双层结构:Serializer + Formatter。
Serializer 负责语义(把 Data Model 翻译成 JSON 结构),Formatter 负责格式(具体怎么写字节——紧凑 vs 缩进、转义策略)。
rust
pub trait Formatter {
fn write_bool<W: io::Write>(&mut self, writer: &mut W, value: bool) -> io::Result<()>;
fn write_i8<W: io::Write>(&mut self, writer: &mut W, value: i8) -> io::Result<()>;
// ...
fn begin_object<W: io::Write>(&mut self, writer: &mut W) -> io::Result<()>; // 写 {
fn end_object<W: io::Write>(&mut self, writer: &mut W) -> io::Result<()>; // 写 }
fn begin_object_key<W: io::Write>(&mut self, writer: &mut W, first: bool) -> io::Result<()>;
fn end_object_key<W: io::Write>(&mut self, writer: &mut W) -> io::Result<()>;
fn begin_object_value<W: io::Write>(&mut self, writer: &mut W) -> io::Result<()>;
fn end_object_value<W: io::Write>(&mut self, writer: &mut W) -> io::Result<()>;
// ... 其他 begin/end 方法
}两个实现:
CompactFormatter:紧凑输出{"a":1,"b":2}PrettyFormatter:缩进输出{\n "a": 1,\n "b": 2\n}
用户选 to_string_pretty 还是 to_string 决定用哪个 formatter。
这种"语义/格式" 解耦让 serde_json 的同一套核心逻辑服务两种输出风格。也让用户可以自定义 Formatter(自己实现 Formatter trait,改变缩进、引号风格等)——一个扩展口。
2. 所有 7 个 SerializeXxx 都是 Compound。
rust
pub enum Compound<'a, W: 'a, F: 'a> {
Map { ser: &'a mut Serializer<W, F>, state: State },
// 在某些 feature 下还有其他变体
}
enum State {
Empty,
First,
Rest,
}一个 Compound 同时服务 Seq、Tuple、Map、Struct 等——通过内部 State 标记进度。这让代码量显著减少(不需要为每种状态机写独立类型)。
看它同时实现 SerializeSeq 和 SerializeStruct:
rust
impl SerializeSeq for Compound<'_, W, F> {
type Ok = ();
type Error = Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<()> {
match *self {
Compound::Map { ref mut ser, ref mut state } => {
ser.formatter
.begin_array_value(&mut ser.writer, *state == State::First) // 写 , 或 nothing
.map_err(Error::io)?;
*state = State::Rest;
tri!(value.serialize(&mut **ser)); // 递归序列化元素
ser.formatter
.end_array_value(&mut ser.writer)
.map_err(Error::io)?;
Ok(())
}
}
}
fn end(self) -> Result<()> {
match self {
Compound::Map { ser, state } => {
match state {
State::Empty => {}
_ => tri!(ser.formatter.end_array(&mut ser.writer).map_err(Error::io)),
}
Ok(())
}
}
}
}注意 end_array 依赖 state——如果一个元素都没序列化过(state == Empty),跳过 end_array;否则正常写 ]。这是处理"空数组"边缘情况的优雅方式。
3. 状态机通过 State enum 手动管理。
rust
enum State {
Empty, // 还没有元素
First, // 第一个元素(不写前置逗号)
Rest, // 非第一个元素(写前置逗号)
}这个 State 穿梭在 serialize_element、serialize_field 等方法之间。每次添加元素前检查 State,决定要不要先写分隔符。写完把 State 推进到 Rest。
手动状态管理是 JSON 序列化必然要做的事——因为 JSON 的 [1,2,3] 里元素之间有逗号,第一个没有、最后一个也没有(JSON 不接受尾随逗号)。State 机记住"我现在在哪",让每次调用决策简单。
17.5 真实序列化流程追踪
用户代码:
rust
let user = User { id: 1, name: "alice".into() };
let json = serde_json::to_string(&user)?;调用栈(简化):
to_string(&user)→ 创建Vec<u8>→ 调to_writer(&mut vec, &user)to_writer(w, value)→ 创建Serializer::new(w)→value.serialize(&mut serializer)User::serialize(&self, &mut serializer)→ 走 derive 生成的代码- 生成代码调
serializer.serialize_struct("User", 2)→ 返回 Compound::Map(state=Empty) - 生成代码调
compound.serialize_field("id", &self.id)- formatter 写
{+"id"+: - 递归
1u64.serialize(&mut *serializer)→ serialize_u64 → formatter.write_u64 写1 - state 推进到 First
- formatter 写
- 生成代码调
compound.serialize_field("name", &self.name)- formatter 写
,+"name"+: - 递归
"alice".serialize(...)→ serialize_str → formatter.write_string 写"alice" - state 推进到 Rest
- formatter 写
- 生成代码调
compound.end()→ formatter 写} - 最终 writer 里是
{"id":1,"name":"alice"} to_string把 writer unsafe 转 String 返回
每一步都是 Data Model 协议的调用 + Formatter 的字节写入。第 3 章的抽象在这里完全落地。
17.6 Deserializer 实现:2704 行做什么
serde_json::Deserializer 比 Serializer 复杂——反序列化要处理"解析 JSON token 流"。
rust
// serde_json/src/de.rs:31
pub struct Deserializer<R> {
read: R,
scratch: Vec<u8>,
remaining_depth: u8,
// ...
}
impl<'de, 'a, R: read::Read<'de>> de::Deserializer<'de> for &'a mut Deserializer<R> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let peek = match tri!(self.parse_whitespace()) {
Some(b) => b,
None => return Err(self.peek_error(ErrorCode::EofWhileParsingValue)),
};
let value = match peek {
b'n' => { /* null */ ... visitor.visit_unit() ... }
b't' | b'f' => { /* true/false */ ... visitor.visit_bool(...) ... }
b'"' => { /* string */ ... visitor.visit_borrowed_str / visit_string ... }
b'[' => { /* array */ ... visitor.visit_seq(SeqAccess { ... }) ... }
b'{' => { /* object */ ... visitor.visit_map(MapAccess { ... }) ... }
b'0'..=b'9' | b'-' => { /* number */ ... visitor.visit_u64/i64/f64 ... }
_ => return Err(self.peek_error(ErrorCode::ExpectedSomeValue)),
};
value
}
// deserialize_bool, deserialize_i32, ... 大多数直接转发到 deserialize_any
// 或者做类型特定优化
}关键机制:
1. deserialize_any 是 JSON 反序列化的主引擎。
因为 JSON 是自描述格式(见第 4 章)——看到 "...." 知道是字符串、[...] 知道是数组、{...} 知道是对象。deserialize_any 根据下一个字节判断类型,调用对应的 visit 方法。
2. 其他 deserialize_* 方法可能直接转发或做优化。
rust
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
let peek = match tri!(self.parse_whitespace()) {
Some(b) => b,
None => return Err(self.peek_error(ErrorCode::EofWhileParsingValue)),
};
let value = match peek {
b't' => {
self.eat_char();
tri!(self.parse_ident(b"rue"));
visitor.visit_bool(true)
}
b'f' => {
self.eat_char();
tri!(self.parse_ident(b"alse"));
visitor.visit_bool(false)
}
_ => Err(self.peek_invalid_type(&visitor)), // 不是 bool,错误指向期望类型
};
// ...
}deserialize_bool 不走 any——它立刻验证下一个字节是 't' 或 'f',不是就报"invalid type"错。这种优化让"类型确定时"的反序列化快很多——不用建立 number/string 等一堆中间状态。
3. SeqAccess 和 MapAccess 的实现。
看数组解析(简化):
rust
struct SeqAccess<'a, R: 'a> {
de: &'a mut Deserializer<R>,
first: bool,
}
impl<'de, 'a, R: Read<'de> + 'a> de::SeqAccess<'de> for SeqAccess<'a, R> {
type Error = Error;
fn next_element_seed<T: DeserializeSeed<'de>>(&mut self, seed: T) -> Result<Option<T::Value>> {
let peek = match tri!(self.de.parse_whitespace()) {
Some(b) => b,
None => return Err(self.de.peek_error(ErrorCode::EofWhileParsingList)),
};
if peek == b']' {
// 数组结束
self.de.eat_char();
return Ok(None);
}
if !self.first {
// 不是第一个元素,必须有 ','
if peek != b',' {
return Err(self.de.peek_error(ErrorCode::ExpectedListCommaOrEnd));
}
self.de.eat_char(); // 消耗 ','
}
self.first = false;
// 解析下一个元素
Ok(Some(tri!(seed.deserialize(&mut *self.de))))
}
}逻辑就是"解析一个元素,返回 Some(element) 或 None(遇到 ])"。状态机的精度体现在每种出错情况都有独立错误码(EofWhileParsingList、ExpectedListCommaOrEnd 等)——方便用户调试。
17.7 read::Read trait:输入抽象
JSON 可能从多种源来——&str、&[u8]、io::Read。serde_json 用 read::Read trait 统一处理:
rust
// serde_json/src/read.rs
pub trait Read<'de> {
fn next(&mut self) -> Result<Option<u8>>; // 读一个字节
fn peek(&mut self) -> Result<Option<u8>>; // 看下一个字节(不消耗)
// 读字符串,可能借用
fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'de, 's, str>>;
// ...
}
pub enum Reference<'b, 'c, T: ?Sized + 'static> {
Borrowed(&'b T), // 借用原输入
Copied(&'c T), // 从 scratch 借用
}三种实现:
StrRead<'a>:从&'a str读。可以 borrow 原字节(Reference::Borrowed)。SliceRead<'a>:从&'a [u8]读。类似 StrRead。IoRead<R>:从io::Read读。必须复制(读过就走了),只能Reference::Copied。
第 15 章讲的借用反序列化在这里有具体支撑。StrRead 的 parse_str 返回 Reference::Borrowed(&'de str) → visitor 的 visit_borrowed_str 被调用 → &'de str 字段可以零拷贝。IoRead 的 parse_str 必须把字节复制到 scratch,返回 Reference::Copied(&'c str) → visitor 的 visit_str(非 borrowed)→ &'de str 字段的 visitor 报 invalid_type 错。
这个设计把"能否借用"的选择权放到了Read 实现层——上层的 Visitor 被动接受。零拷贝能力从格式、输入源、数据类型三方协商——第 15 章讲的"三方博弈"在这里有源码层的支撑。
17.8 字符串解析的精细:转义处理
JSON 字符串可能有转义序列(\n、\t、\"、\uFFFF)。解析时:
无转义字符串:"alice"——直接借用原字节。
有转义字符串:"hello\nworld"——必须解码,写到 scratch 里:
rust
// serde_json/src/read.rs (精简)
fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'de, 's, str>> {
let start = self.index;
loop {
match self.peek()? {
Some(b'"') => {
// 如果没遇到过转义,直接借用
if scratch.is_empty() {
let s = &self.slice[start..self.index];
self.index += 1; // 跳过 "
return Ok(Reference::Borrowed(unsafe { str::from_utf8_unchecked(s) }));
} else {
// 之前遇到过转义,已经复制到 scratch
scratch.extend_from_slice(&self.slice[start..self.index]);
self.index += 1;
return Ok(Reference::Copied(unsafe { str::from_utf8_unchecked(scratch) }));
}
}
Some(b'\\') => {
// 遇到转义 — 首次遇到时复制前置部分到 scratch
scratch.extend_from_slice(&self.slice[start..self.index]);
self.parse_escape(scratch)?; // 解码转义写进 scratch
start = self.index; // 更新起点
}
_ => self.index += 1,
}
}
}逻辑:一路扫描,遇到 " 结束、遇到 \ 时把已扫描部分复制到 scratch、解码转义后继续。无转义的字符串完全零拷贝;有转义的最少复制(只复制到 scratch 里一次)。
**这种"optimistic borrowing"**是 serde_json 性能的重要来源:常见 JSON 字符串没有反斜杠转义时,可以直接借用输入切片;一旦遇到转义,再退回 scratch 缓冲。具体命中率取决于业务数据,日志、自然语言、URL、Unicode 转义密集的输入会有完全不同的表现。
17.9 Value:动态 JSON 表示
serde_json::Value(value/mod.rs)是"通用 JSON 对象":
rust
pub enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec<Value>),
Object(Map<String, Value>),
}用途:
- 当你不知道 JSON 形状:解析成
Value,运行时探索。 - 当你要动态构造 JSON:用
json!宏或手动构建 Value 树。 - 当你要转换:从一个 struct 转 JSON 再转另一个 struct(
serde_json::from_value(to_value(x)?))。
Value 的 Serialize 实现(value/ser.rs,简化):
rust
impl Serialize for Value {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match *self {
Value::Null => serializer.serialize_unit(),
Value::Bool(b) => serializer.serialize_bool(b),
Value::Number(ref n) => n.serialize(serializer),
Value::String(ref s) => serializer.serialize_str(s),
Value::Array(ref v) => v.serialize(serializer),
Value::Object(ref m) => {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(m.len()))?;
for (k, v) in m {
map.serialize_entry(k, v)?;
}
map.end()
}
}
}
}match 分派到对应 Serializer 方法——每种 Value 变体映射到 Data Model 的某个原语。
Value 的 Deserialize 实现(value/de.rs,简化):
rust
impl<'de> Deserialize<'de> for Value {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Value, D::Error> {
struct ValueVisitor;
impl<'de> Visitor<'de> for ValueVisitor {
type Value = Value;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("any valid JSON value")
}
fn visit_bool<E: Error>(self, v: bool) -> Result<Value, E> { Ok(Value::Bool(v)) }
fn visit_i64<E: Error>(self, v: i64) -> Result<Value, E> { Ok(Value::Number(v.into())) }
fn visit_u64<E: Error>(self, v: u64) -> Result<Value, E> { Ok(Value::Number(v.into())) }
fn visit_f64<E: Error>(self, v: f64) -> Result<Value, E> { Ok(Value::Number(Number::from_f64(v).unwrap())) }
fn visit_str<E: Error>(self, v: &str) -> Result<Value, E> { Ok(Value::String(v.to_owned())) }
fn visit_string<E: Error>(self, v: String) -> Result<Value, E> { Ok(Value::String(v)) }
fn visit_none<E: Error>(self) -> Result<Value, E> { Ok(Value::Null) }
fn visit_some<D: Deserializer<'de>>(self, d: D) -> Result<Value, D::Error> { Deserialize::deserialize(d) }
fn visit_unit<E: Error>(self) -> Result<Value, E> { Ok(Value::Null) }
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
let mut vec = Vec::new();
while let Some(elem) = seq.next_element()? { vec.push(elem); }
Ok(Value::Array(vec))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
let mut values = Map::new();
while let Some((k, v)) = map.next_entry()? { values.insert(k, v); }
Ok(Value::Object(values))
}
}
d.deserialize_any(ValueVisitor)
}
}关键:d.deserialize_any(ValueVisitor) —— 调用 deserialize_any,让 Deserializer 根据输入形态分派到 visitor 的对应方法。这也是 Value 只支持自描述格式(JSON、YAML、MessagePack)而不能从 Bincode 反序列化的原因——Bincode 没有 deserialize_any(第 4 章讲过)。
17.9.1 Number 的内部三态:N::PosInt / NegInt / Float
上面 Value::Number(Number) 的 Number 看着像一个简单数字容器——实际打开 serde-json/src/number.rs:22 会看到它是一个刻意设计的三态代数类型:
rust
pub struct Number {
n: N,
}
#[cfg(not(feature = "arbitrary_precision"))]
enum N {
PosInt(u64),
/// Always less than zero.
NegInt(i64),
/// Always finite.
Float(f64),
}
#[cfg(feature = "arbitrary_precision")]
type N = String;三态的存储选择不是随手定的:
PosInt(u64)——正整数用u64,可表示 0 到2^64 - 1。如果用i64只能到2^63 - 1、一半的正数范围浪费掉。NegInt(i64)——严格负整数(源码注释 "Always less than zero")。用i64能表示-2^63到-1。Float(f64)——源码注释 "Always finite"——NaN、+inf、-inf永远不会存进这个变体。构造函数(如from_f64)会显式拒绝非有限值。
两个"总是"约束不是描述性的 docstring、是代码层面的不变式——正因为有这两个约束,N 才能实现 Eq(line 50):
rust
// Implementing Eq is fine since any float values are always finite.
#[cfg(not(feature = "arbitrary_precision"))]
impl Eq for N {}NaN 有 "NaN != NaN" 的反射性破坏——如果 Float 可能是 NaN、Eq(要求反射性)就不能实现。Serde_json 通过在构造时拒绝 NaN换来"Number 可以用作 HashMap 的 key"——用户失去"在 JSON 里表示 NaN"的能力(JSON 规范也不支持)、换来 Number 的 Hash + Eq 语义完整性。
17.9.2 +0.0 / -0.0 的 Hash 归一化
N 的 Hash 实现(line 53-69)藏着 IEEE 754 浮点数规范的一个经典坑:
rust
impl Hash for N {
fn hash<H: Hasher>(&self, h: &mut H) {
match *self {
N::PosInt(i) => i.hash(h),
N::NegInt(i) => i.hash(h),
N::Float(f) => {
if f == 0.0f64 {
// There are 2 zero representations, +0 and -0, which
// compare equal but have different bits. We use the +0 hash
// for both so that hash(+0) == hash(-0).
0.0f64.to_bits().hash(h);
} else {
f.to_bits().hash(h);
}
}
}
}
}IEEE 754 里 +0.0 和 -0.0 是不同位模式但 == 相等。按 Rust 的 Hash-Eq 契约——a == b 必须蕴含 hash(a) == hash(b)——直接用 to_bits() 会违反契约(两个位模式不同的 f64 得到不同 hash)。源码的解决是检测 f == 0.0 就统一用 +0.0 的 bits 做 hash——让两个零的 hash 相同。
这 10 行代码是 Rust 程序员"读标准 + 读 f64 语义"修炼的试金石:
- 不读 IEEE 754:看不出为什么要这个
if - 读了 IEEE 754 但没读 Rust Hash-Eq 契约:看不出不做归一的后果
- 两个都读了:才明白这是不可省略的正确性修补
17.9.3 arbitrary_precision feature:String 作存储的另一面
最后一个 cfg 分支(line 72-73)值得一看:
rust
#[cfg(feature = "arbitrary_precision")]
type N = String;用户 features = ["arbitrary_precision"] 开启后、Number 不再按类型分三态、而是直接存原始 JSON 文本。目的:保留任意精度——比如 JSON 里出现 123456789012345678901234567890(超出 u64)或 0.1 + 0.2 这种浮点精度问题,标准三态表示必然丢精度;存成 String 可以保留原样、延迟到用户显式转类型时再 round-trip。
代价也很真实——+/-/* 等数值操作全部不可用(String 不支持),所有 as_i64() 类访问都要走字符串解析。这个 feature 在高精度金融 / 科学计算场景必选、在普通 Web API场景就是多余开销。一个 type alias 分支把两种完全不同的存储策略压进同一个 API——用户代码不变、只改 Cargo.toml。
这三个细节(三态代数存储 / Hash 归一 / arbitrary_precision feature 切换)合在一起是 "JSON Number 在 Rust 里怎么做才正确"的完整答案——每一条都对应一个容易忽略但影响正确性或表达力的具体问题。
17.10 性能工程:lexical 浮点数库
serde_json 源码里有一个 lexical/ 目录(实测 3708 行、9 个 .rs 文件)。它是什么?
IEEE 754 浮点数的字符串往返格式化——非常难。直接用 Rust 标准库的 {} 格式化 f64 可能不是最优(字符串更长、精度可能不完全)。serde_json 引入了一个独立的 lexical 实现,在浮点数→字符串的往返中保证:
- 最短表示:
0.1f64格式化为"0.1"而不是"0.1000000000000000055511151231257827021181583404541015625"。 - 往返精确:
f64 → str → f64得回完全一致的值(比特一致)。 - 比标准库快:对浮点数 Display 格式化有显著提升(社区测评通常报告 2-5 倍量级,具体依赖浮点值形态;参考 Ryū 论文的基准测试)。
这 3708 行代码是 "performance-critical" 工程的典型。对于浮点字段密集的 JSON,数值格式化会成为关键路径;对于字符串或对象结构占主导的 JSON,瓶颈可能转向转义、map 遍历或写入器。优化它的价值来自"浮点格式化很难且经常出现在热路径",不来自某个脱离场景的固定比例。
启示:一个好的格式实现需要的不只是"实现 Serde trait"——还要工业级的底层工程。数值格式化、字符串转义、错误定位(行列号)、内存分配策略——每一处都是性能/质量的关键。
17.11 to_value / from_value:中间类型的双向转换
serde_json 提供一个特别有用的函数对:
rust
pub fn to_value<T: Serialize>(value: T) -> Result<Value>;
pub fn from_value<T: DeserializeOwned>(value: Value) -> Result<T>;to_value:把任何 Serialize 类型转成 Value 树。 from_value:把 Value 树转成任何 DeserializeOwned 类型。
实现原理:它们内部创建一个"Value 作为 Serializer 或 Deserializer"的 adapter:
rust
// to_value 伪代码
pub fn to_value<T: Serialize>(value: T) -> Result<Value> {
value.serialize(Serializer) // Serializer 是一个构造 Value 的 Serializer
}
// Serializer 定义
struct Serializer;
impl ser::Serializer for Serializer {
type Ok = Value;
type Error = Error;
fn serialize_bool(self, v: bool) -> Result<Value> { Ok(Value::Bool(v)) }
fn serialize_str(self, v: &str) -> Result<Value> { Ok(Value::String(v.to_owned())) }
fn serialize_seq(self, _: Option<usize>) -> Result<SerializeVec> { Ok(SerializeVec::new()) }
// ...
}注意 type Ok = Value——Serializer 的 Ok 是返回值!不是 ()(写入副作用)。这是第 3 章提到过的"构建式 Serializer"——用 type Ok 传递返回值。
用途:任何 Serialize 类型都可以转成 Value 做运行时处理:
rust
let user = User { id: 1, name: "alice".into() };
let mut value = serde_json::to_value(&user)?;
value["extra"] = json!("metadata"); // 运行时修改
let modified_user: UserExtended = serde_json::from_value(value)?;性能代价:这一对函数内部构建完整 Value 树——对大对象有显著内存开销。但功能价值远超性能代价——很多应用 API 需要"拿到数据、做点运行时判断、再重新发给别的 API",用 Value 是最自然的表达。
17.12 json! 宏
serde_json 还提供一个声明宏方便构造 Value:
rust
use serde_json::json;
let v = json!({
"status": "ok",
"code": 200,
"data": {
"id": 1,
"tags": ["a", "b", "c"]
}
});
// v: Value实现在 macros.rs(303 行声明宏)——第 5 章讲过这是声明宏而不是过程宏。它递归匹配 JSON-like 语法,展开成 Value::Object / Value::Array 等构造调用。
声明宏的典型应用——不需要 AST 操作,只是"模板化构造"。对这种场景,声明宏足够用,且编译快得多。
17.12.1 serde_json 整 crate 18276 行的真实尺寸表
把整个 serde-rs/json 仓库 src/ 按文件大小排序——
| 文件 | 行 | 角色 |
|---|---|---|
de.rs | 2704 | Deserializer + 字符串转义 + 数字解析 + 错误恢复(§17.6 主角) |
ser.rs | 2285 | Serializer + 9 种 SerializeXxx Compound + Formatter trait(§17.4 主角) |
value/de.rs | 1507 | Value → T 反序列化(DeserializeAny 的 in-memory 路径) |
map.rs | 1181 | serde_json::Map——BTreeMap / IndexMap 的 wrapper(feature 切换) |
read.rs | 1089 | StrRead / SliceRead / IoRead 三套 Read trait(§17.7) |
value/ser.rs | 1063 | T → Value 序列化 |
value/mod.rs | 1042 | Value enum 定义 + 各种 as_x() 访问器 |
lexical/math.rs | 884 | 大整数运算(浮点格式化的内部表示) |
number.rs | 814 | Number 类型(§17.9.1) |
raw.rs | 784 | RawValue——延迟解析的 newtype(用于子 JSON 透传) |
lexical/large_powers64.rs | 625 | 预计算的 10 的幂表(64-bit) |
error.rs | 541 | Error 类型 + 行列号定位 |
lib.rs | 441 | crate 入口 + from_str / to_string / to_value 等顶层 API |
lexical/num.rs | 421 | 数字 trait 抽象 |
macros.rs | 303 | json! 宏的 token tree 递归实现 |
value/from.rs | 284 | From<i32> for Value 等 70+ impl |
value/index.rs | 258 | Index trait 让 value["a"]["b"] 可写 |
| 其余 lexical 7 文件 | 132~231 | rounding / cached_float80 / bhcomp / algorithm / float / large_powers32 / errors |
io/core.rs + iter.rs 等小文件 | ≤ 100 | no_std 兼容 + 工具 |
| 总计 | 18276 |
四条值得记住的物理事实——
- de.rs > ser.rs(2704 vs 2285)——反序列化比序列化大 19%——根因是反序列化要做两件序列化没有的事:(a) 字节流的字符级状态机(去 BOM、跳空白、识别字面量);(b) 错误恢复(位置追踪、unexpected_token 报告)——
error.rs541 行有近一半在算 line/column value/de.rs(1507)比value/ser.rs(1063)大 41%——因为 Value → T 的反序列化要为Value 的每种 variant 写一遍 visit_xxx 方法(数字 → i64/u64/f64 / bool / 字符串 / null / map / seq);T → Value 反过来只需要"接收 visit_xxx 调用、组装成 Value enum"——visitor 模式的不对称代价- lexical 子目录 3708 行 = 整 crate 20%——纯粹用于浮点格式化——其中
large_powers64.rs625 +large_powers32.rs183 = 808 行只是预计算 10 的幂查表——为了避免运行时算pow(10, n)损失精度 raw.rs单独 784 行——RawValue是个看似简单的 newtype({"raw": "<bytes>"})、但要正确实现 Serialize/Deserialize 需要和 serde 内部的__private字段名约定握手——所以 serde 主仓库里有Content/ContentDeserializer专门为 RawValue 服务(§14 讲过)
17.12.2 三个容易忽视的格式边界:Map、RawValue、StreamDeserializer
理解 serde_json 不能只看 from_str / to_string。真实项目里经常踩坑的,是三个边界类型:对象 map、原始 JSON 片段、连续 JSON 流。
第一,Map<String, Value> 默认不是插入有序。 serde_json/src/map.rs:1-4 的模块注释写明:默认 backing 是 BTreeMap,开启 preserve_order feature 才改用 IndexMap。类型别名在 serde_json/src/map.rs:33-36 落地:未开启 feature 时 MapImpl<K, V> = BTreeMap<K, V>,开启时是 IndexMap<K, V>。
这影响所有依赖 Value::Object 的代码。你如果把 JSON 当"有序配置文件"处理,默认 serde_json::Value 会按 key 排序,而不是保留输入顺序。想保留顺序,要在依赖里显式开启 feature。这个行为不是文档角落里的小字,而是 map 类型的源码结构决定的。
serde_json/src/map.rs:491-503 给 Map<String, Value> 实现 Serialize,内部调用 serializer.serialize_map(Some(self.len())) 并逐项 serialize_entry;serde_json/src/map.rs:506-536 给它实现 Deserialize,用 Visitor 的 visit_map 循环填充新 map。serde_json/src/map.rs:601-615 还为 owned 和 borrowed map 实现 IntoDeserializer,这让内存里的 Value::Object 可以再次作为反序列化输入,支撑 from_value 这类 API。
第二,RawValue 是"延迟解析"边界,不是普通字符串。 serde_json/src/raw.rs:18-27 说明它用于推迟解析,或者完全不解析、只把一段 payload 原样转发;序列化时保留原始格式,不会被 minify 或 pretty-print。serde_json/src/raw.rs:81-100 给出两个形态:从 from_str / from_slice 进入时可以借用 &RawValue,从 from_reader 进入时必须用 boxed 形态,因为 I/O 流需要把原始片段缓冲到内存。
serde_json/src/raw.rs:116-132 把 RawValue 定义成 repr(transparent) 的 str newtype,并用受控的 transmute 在 &str / Box<str> 和 RawValue 之间转换。这段实现说明 RawValue 的语义是"这段字符串已经被验证为 JSON 值",不是"任意字符串以后再说"。它适合网关、日志管道、签名透传这类场景:外层结构由 Serde 检查,内层大 JSON 片段保持原貌。
第三,StreamDeserializer 是连续值协议的边界。 serde_json/src/de.rs:2330-2356 定义 StreamDeserializer,文档示例展示同一输入里连续放对象、数字、字符串、对象和数组;serde_json/src/de.rs:2434-2450 的 Iterator::next 每次跳过空白后反序列化一个 T。这不是 JSON array,而是"多个自分界 JSON 值连续排列"。
这三个边界对应三类工程决策:
| 决策 | 默认行为 | 什么时候要显式调整 |
|---|---|---|
| 对象 key 顺序 | BTreeMap 排序 | 需要保留输入顺序时启用 preserve_order |
| 子 JSON 是否解析 | 普通字段会完整解析 | 只透传或延迟处理时用 RawValue |
| 输入是否只有一个值 | from_str 期待一个完整值 | 日志流、拼接响应、增量协议用 StreamDeserializer |
把这些边界纳入设计,比在业务层补丁式处理更稳。比如 LLM 工具调用返回一串 JSON 片段时,不应先手写 split;应该用 StreamDeserializer 判断每个片段是否是完整 JSON 值。API 网关需要转发未知字段时,不应把大对象解析成 Value 再写回;如果只需要原样透传,可以考虑 RawValue。配置文件需要保留用户书写顺序时,不应假设 Value::Object 默认有序,而要明确 feature 选择。
还有一个与第 3 章呼应的边界:JSON map key。serde_json/src/ser.rs:2240-2244 的 to_string 文档写明,序列化可能因为 map 含非字符串 key 而失败。这个错误不是 Value 层才会出现;任何 HashMap<K, V> 只要 K 的序列化结果不是 JSON 字符串,都可能在格式层被拒绝。Serde Data Model 允许 map key 是任意可序列化值,但 JSON 语法只允许字符串 key。格式 crate 的职责,就是在 Data Model 的宽表达力和具体格式的窄语法之间做裁决。
所以 serde_json 不是"Serde trait 的一个简单实现"。它同时承担四个约束转换:Rust 类型到 JSON 语法、Serde map 到字符串 key 对象、输入切片到可借用生命周期、连续字节流到一个个自分界 JSON 值。只看 Value enum 会低估它的复杂度;真正的格式实现难点,恰好藏在这些边界里。
这也解释了为什么 serde_json 适合作为学习其他格式 crate 的参照物。你先在 JSON 里看清这些边界,再去读 bincode、postcard、rmp-serde,就会知道哪些复杂度来自 Serde,哪些复杂度来自格式本身。
读格式 crate 时可以沿用同一张清单:值如何进入,复合结构如何保持状态,错误如何定位,借用是否可能,动态树和流式解析是否存在。serde_json 把这些问题都摆在明处。
掌握这张清单后,再选择 JSON、MessagePack、Bincode 或 Postcard,就不只是比较体积和速度,而是在比较各自能表达什么、不能表达什么、错误会在哪里出现。
格式选择最终是语义选择,也是故障边界选择。
17.13 和其他 Serde 格式 crate 的对比
知道 serde_json 的实现后,看其他格式 crate 会更快:
- bincode:二进制紧凑格式。它的 Serializer 没有 Formatter 层——直接按二进制布局写。不支持
deserialize_any(非自描述)。代码量约 5000 行(serde_json 的一半)。 - rmp-serde(MessagePack):二进制自描述格式。实现和 serde_json 类似,有 Formatter 对应 MessagePack 的不同变体(有无字段名等)。支持
deserialize_any。 - serde_yaml:基于 yaml-rust,JSON 风格但语法更复杂。代码最大(因为 YAML 语法复杂度远超 JSON)。
- toml:TOML 格式。实现不同——TOML 是"无上下文的 key-value"格式,反序列化时常要预扫描。
- postcard:极简二进制格式,针对嵌入式。高度优化,代码量约 3000 行。
所有 serde 格式 crate 共享同一套 Serde trait——用户只需学一次 API 就能切换任何格式。这是第 1 章讲的 M+N 架构的实际威力。
17.14 和丛书其他书的关联
serde_json 和丛书里多个书有深度关联:
- 丛书《MCP 协议设计与实现》第 3 章"JSON-RPC 与消息格式":所有 MCP 消息通过 serde_json 序列化/反序列化。读过那本书再回来看本章,你能理解 MCP runtime 的每一次 JSON 往返用的就是本章讲的 Serializer/Deserializer。
- 丛书《LangChain 设计与实现》:LangChain 的所有 Tool 调用、LLM response 都用 serde_json 解析。Agent 的"lose ends" 问题(处理 LLM 偶尔返回格式错误 JSON)很多时候就是本章讨论的错误恢复逻辑在用户层的表现。
- 丛书《Tokio 源码深度解析》第 17 章 "可观测性":tracing 输出 JSON 日志时调用 serde_json——高频调用场景下 serde_json 的性能设计(Formatter 分离、lexical 浮点格式化)就是让 tracing 不成为性能瓶颈的直接原因。
- 丛书卷一《Rust 编译器》第 7 章"trait 分发":serde_json Serializer 的每个方法都是泛型/单态化——编译器为每种"数据类型 × Serializer" 组合生成单独代码。这就是卷一单态化那一章的实际应用。
17.15 本章小结
serde_json 是 Serde 生态"格式实现"的参考标杆。本章讲了它的核心设计:
- 分层架构:Serializer(语义层)+ Formatter(格式层)——解耦"Data Model 转换"和"字节输出风格"。
- Compound 状态机:7 个 SerializeXxx 关联类型全部是 Compound——通过 State enum 管理进度,极大减少代码量。
- 手写 JSON 解析器:de.rs 2700+ 行手写字符、状态机、错误定位。
deserialize_any是主引擎,其他 deserialize 方法做类型特定优化。 - Read trait 的输入抽象:StrRead / SliceRead / IoRead 三种实现。StrRead 和 SliceRead 支持
Reference::Borrowed零拷贝,IoRead 必须复制——这是第 15 章借用反序列化的底层支撑。 - Value 动态树:用 enum + DeserializeAny 支持运行时 JSON 操作。
- 性能工程:lexical 浮点库 3708 行 9 文件专门优化浮点格式化——说明"格式实现"不只是 trait 实现,还有工业级底层工程。
serde_json 整 crate 18276 行——de.rs (2704) + ser.rs (2285) + lexical/ (3708) 三块占 48%——是理解整个 JSON 实现的最小必读子集。
动手实验
- 测试 pretty 和 compact 的区别:用
serde_json::to_string_pretty和to_string序列化同一个对象,观察输出差异。然后读ser.rs的 PrettyFormatter 实现,看它如何添加换行和缩进。 - 手写一个 Formatter:实现一个 "YAML-like Formatter",让 serde_json 输出 YAML 风格的 JSON。不需要完整,实现
begin_object/end_object/begin_object_key等几个方法就好。 - 测试 StrRead 和 IoRead 的性能差异:用同一份 JSON,一次
from_str、一次from_reader(用 Cursor 包装同样的字节)。用 criterion 对比时间。注意不同的 struct(有&str字段 vs 全 String 字段)。 - 读 lexical/:打开浮点格式化库,理解"最短表示"算法(Grisu3 或 Ryū)。这是一个独立的小引擎,读懂它你能理解"为什么浮点格式化是个非平凡问题"。
延伸阅读
- serde_json 仓库:完整源码。2700 行的 de.rs 是整个仓库最有"工程感"的一份代码。
- JSON RFC 8259:JSON 格式规范,读懂 serde_json 对各种边缘情况(空对象、嵌套深度限制、Unicode 处理)的处理选择。
- Ryū 浮点格式化论文:lexical 使用的浮点格式化算法。深度内容,但对性能工程师必读。
- Other Serde formats:bincode、postcard、rmp-serde、serde_yaml 等格式 crate。
- 丛书《MCP 协议设计与实现》第 3 章:看 serde_json 在真实 JSON-RPC 协议里的应用。
- 丛书卷一《Rust 编译器》第 7 章:"trait 分发" 解释为什么 serde_json 的泛型方法能零成本。