Serde 元编程

第 17 章 serde_json 源码:手写解析器如何接入 Data Model

作者 杨艺韬 · 9,284 字 · 发布于 · 更新于

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.13 的尺寸表。本章不逐行讲这些源码,而是聚焦一个核心问题:

一个格式实现者如何把自己"接入" Serde?

回答这个问题需要看三部分源码:

  1. serde_json::Serializer(ser.rs,2285 行):如何实现 Serializer trait
  2. serde_json::Deserializer(de.rs,2704 行):如何实现 Deserializer trait
  3. serde_json::Value(value/ 目录):如何用 enum 表示任意 JSON

读完本章你会建立一个具体参照系——"当我要为我的格式写 Serde 支持时,代码应该长这样"。

本专栏基于 serde_json 1.0.149(commit dc8003a)。本章写成 src/... 的路径都指 serde_json 仓库内的路径;serde 仓库的路径照常写成 serde_core/src/...、serde/src/...。

17.2 源码结构

先看文件组织:

src/                (serde_json 仓库)
├── 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>(默认 BTreeMap,开 preserve_order 才是 IndexMap)
├── number.rs       814 行  Number 类型(i64/u64/f64 统一表示)
├── error.rs        541 行  Error 类型
├── raw.rs          784 行  RawValue(不解析的 JSON 片段)
├── macros.rs       303 行  json! 宏
├── iter.rs          70 行  LineColIterator(IoRead 的行列号计数)
└── lexical/        子目录  浮点数解析库(十进制文本 → f64;输出方向交给 zmij crate)

三大支柱:

  • 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

最常用的两个函数(src/de.rs:2699 和 src/ser.rs:2245):

// 简化版
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 vec = to_vec(value)?;          // to_vec 内部:Vec::with_capacity(128) + to_writer
    // SAFETY: We do not emit invalid UTF-8.
    let string = unsafe { String::from_utf8_unchecked(vec) };
    Ok(string)
}

from_str 用 StrRead 包装 &'a str('a 就是第 15 章的 'de),然后调用通用 from_trait 函数做实际反序列化。

to_string 先经 to_vec 写入 Vec<u8>,然后 unsafe 转 String——因为 serde_json 保证输出总是 valid UTF-8(所有字符串字段都是 str,非字符串字段都是 ASCII)。这个 unsafe 优化省掉 UTF-8 验证(否则 from_utf8 要遍历整个字节数组)。

17.4 Serializer 实现:2285 行做什么

serde_json::Serializer 的 trait 实现是本章的核心之一。看它的结构(src/ser.rs:17):

pub struct Serializer<W, F = CompactFormatter> {
    writer: W,
    formatter: F,
}

impl<'a, 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 的对应方法",
    // serialize_str 走 format_escaped_str,serialize_seq/map 等返回 Compound
}

三个设计要点:

1. 双层结构:Serializer + Formatter。

Serializer 负责语义(把 Data Model 翻译成 JSON 结构),Formatter 负责格式(具体怎么写字节——紧凑 vs 缩进、转义策略)。

// src/ser.rs:1541 起(节选,不是源码原文的连续片段;
// 源码里每个方法都带默认实现、W 的约束是 ?Sized + io::Write,
// 所以 CompactFormatter 只有一行 `impl Formatter for CompactFormatter {}`)
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(src/ser.rs:1939):紧凑输出 {"a":1,"b":2}
  • PrettyFormatter(src/ser.rs:1945):缩进输出 {\n "a": 1,\n "b": 2\n}

用户选 to_string_pretty 还是 to_string 决定用哪个 formatter。

这种"语义/格式" 解耦让 serde_json 的同一套核心逻辑服务两种输出风格。也让用户可以自定义 Formatter(自己实现 Formatter trait,改变缩进、引号风格等)——一个扩展口。

2. 所有 7 个 SerializeXxx 都是 Compound(src/ser.rs:71-77 七个关联类型逐行写着同一个 Compound<'a, W, F>;Compound 定义在 src/ser.rs:471,State 在 src/ser.rs:463)。

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 实现(src/ser.rs:482):

// 简化:省略泛型声明与 where 子句,以及 arbitrary_precision / raw_value 下的 unreachable!() 分支
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——在 SerializeSeq 这条路径上,State::Empty 只在 serialize_seq(Some(0)) 时出现:serialize_seq 已经一口气写完 [](src/ser.rs:284-298),所以 end 什么都不做;其他情况(包括 serialize_seq(None) 后一个元素也没来)都由 end 补上 ]。这是处理"空数组"边缘情况的方式。

3. 状态机通过 State enum 手动管理。

enum State {
    Empty,    // 还没有元素
    First,    // 第一个元素(不写前置逗号)
    Rest,     // 非第一个元素(写前置逗号)
}

这个 State 穿梭在 serialize_element、serialize_field 等方法之间。每次添加元素前检查 State,决定要不要先写分隔符。写完把 State 推进到 Rest。

手动状态管理是 JSON 序列化必然要做的事——因为 JSON 的 [1,2,3] 里元素之间有逗号,第一个没有、最后一个也没有(JSON 不接受尾随逗号)。State 机记住"我现在在哪",让每次调用决策简单。

17.5 真实序列化流程追踪

用户代码:

let user = User { id: 1, name: "alice".into() };
let json = serde_json::to_string(&user)?;

调用栈(简化):

  1. to_string(&user) → 创建 Vec<u8> → 调 to_writer(&mut vec, &user)
  2. to_writer(w, value) → 创建 Serializer::new(w) → value.serialize(&mut serializer)
  3. User::serialize(&self, &mut serializer) → 走 derive 生成的代码
  4. 生成代码调 serializer.serialize_struct("User", 2) → 转到 serialize_map(Some(2)):formatter 先写 {,返回 Compound::Map(state=First)
  5. 生成代码调 compound.serialize_field("id", &self.id)
    • begin_object_key(first = true) 不写逗号;写 "id";begin_object_value 写 :
    • 递归 1u64.serialize(&mut *serializer) → serialize_u64 → formatter.write_u64 写 1
    • state 推进到 Rest
  6. 生成代码调 compound.serialize_field("name", &self.name)
    • begin_object_key(first = false) 写 ,;写 "name";:
    • 递归 "alice".serialize(...) → serialize_str → format_escaped_str(begin_string / write_string_fragment / end_string)写 "alice"
    • state 保持 Rest
  7. 生成代码调 compound.end() → formatter 写 }
  8. 最终 writer 里是 {"id":1,"name":"alice"}
  9. to_string 把 writer unsafe 转 String 返回

每一步都是 Data Model 协议的调用 + Formatter 的字节写入。第 3 章的抽象在这里完全落地。

17.6 Deserializer 实现:2704 行做什么

serde_json::Deserializer 比 Serializer 复杂——反序列化要处理"解析 JSON token 流"。

// 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,各自先看首字节:
    // 整数/浮点走 deserialize_number,char/string/identifier 转给 deserialize_str
}

关键机制:

1. deserialize_any 是 JSON 反序列化的主引擎(src/de.rs:1393)。

因为 JSON 是自描述格式(见第 4 章)——看到 "...." 知道是字符串、[...] 知道是数组、{...} 知道是对象。deserialize_any 根据下一个字节判断类型,调用对应的 visit 方法。

2. 其他 deserialize_* 方法各自做类型特定的检查,不转发给 deserialize_any。

// src/de.rs:1469(源码用 where 子句写 V: de::Visitor<'de>;
// 同文件 :2233 还有一份 MapKey 的同名实现,不是这一处)
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(src/de.rs:1469)不走 any——它立刻验证下一个字节是 't' 或 'f',不是就由 peek_invalid_type 报"invalid type"错(实测 from_str::<bool>("\"t\"") 得到 invalid type: string "t", expected a boolean)。类型确定时,格式层只接受这一种输入,不必走 deserialize_any 的全量分派。

3. SeqAccess 和 MapAccess 的实现。

看数组解析(简化,结构体在 src/de.rs:1919、impl 在 src/de.rs:1930):

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']' {
            // 数组结束;']' 不在这里消耗,留给 deserialize_any 之后的 end_seq
            return Ok(None);
        }

        if !self.first {
            // 不是第一个元素,必须有 ','
            if peek != b',' {
                return Err(self.de.peek_error(ErrorCode::ExpectedListCommaOrEnd));
            }
            self.de.eat_char();  // 消耗 ','
            // ',' 之后紧跟 ']' 是尾随逗号——JSON 不允许
            if let Some(b']') = tri!(self.de.parse_whitespace()) {
                return Err(self.de.peek_error(ErrorCode::TrailingComma));
            }
        }
        self.first = false;

        // 解析下一个元素
        Ok(Some(tri!(seed.deserialize(&mut *self.de))))
    }
}

逻辑就是"解析一个元素,返回 Some(element) 或 None(遇到 ])"。状态机的精度体现在每种出错情况都有独立错误码(EofWhileParsingList、ExpectedListCommaOrEnd、TrailingComma 等;真实代码把这段判断抽成了内部函数 has_next_element,src/de.rs:1937-1962)——方便用户调试。

17.7 read::Read trait:输入抽象

JSON 可能从多种源来——&str、&[u8]、io::Read。serde_json 用 read::Read trait 统一处理:

// src/read.rs:28(`Read`)与 :124(`Reference`)的节选,不是源码原文
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>(src/read.rs:175):从 &'a str 读。可以 borrow 原字节(Reference::Borrowed)。
  • SliceRead<'a>(src/read.rs:164):从 &'a [u8] 读。类似 StrRead——src/read.rs:519 返回 Reference::Borrowed、:523 返回 Reference::Copied。
  • IoRead<R>(src/read.rs:149):从 io::Read 读。必须复制(读过就走了),只能 Reference::Copied——它的 parse_str(src/read.rs:335)与 parse_str_raw(:340)里出现的唯一构造就是 src/read.rs:337、:345 的 .map(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 错(实测:invalid type: string "x", expected a borrowed string)。这是手动 Deserializer::from_reader(..) 再调 T::deserialize 时的运行时表现;走 serde_json::from_reader 会更早失败——它要求 T: DeserializeOwned,含 &str 字段的类型在编译期就被拒绝(implementation of Deserialize is not general enough)。

这个设计把"能否借用"的选择权放到了Read 实现层——上层的 Visitor 被动接受。零拷贝能力由格式、输入源、数据类型三方共同决定——第 15 章讲的借用条件在这里有源码层的支撑。

17.8 字符串解析的精细:转义处理

JSON 字符串可能有转义序列(\n、\t、\"、\uFFFF)。解析时:

无转义字符串:"alice"——直接借用原字节。

有转义字符串:"hello\nworld"——必须解码,写到 scratch 里:

// src/read.rs:494 SliceRead::parse_str_bytes(精简示意:真实代码用 skip_to_escape 批量跳过普通字节、
// 遇到控制字符报错;UTF-8 处理交给 result 回调——StrRead 用 from_utf8_unchecked,SliceRead 做校验)
fn parse_str<'s>(&'s mut self, scratch: &'s mut Vec<u8>) -> Result<Reference<'de, 's, str>> {
    let mut 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(src/value/mod.rs:116)是"通用 JSON 对象":

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 实现(src/value/ser.rs:11,简化):

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 实现(impl 从 src/value/de.rs:22 开始,下面是简化版;其中 visit_f64 在第 69 行):

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(Number::from_f64(v).map_or(Value::Null, Value::Number)) }  // 非有限值落成 Null
            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 看着像一个简单数字容器——实际打开 src/number.rs:22 会看到它是一个刻意设计的三态代数类型:

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

// 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 实现(src/number.rs:53-70)藏着 IEEE 754 浮点数规范的一个经典坑:

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)值得一看:

#[cfg(feature = "arbitrary_precision")]
type N = String;

用户 features = ["arbitrary_precision"] 开启后、Number 不再按类型分三态、而是直接存原始 JSON 文本。目的:保留任意精度——比如 JSON 里出现 123456789012345678901234567890(超出 u64)或 0.1 + 0.2 这种浮点精度问题,标准三态表示必然丢精度;存成 String 可以保留原样、延迟到用户显式转类型时再 round-trip。

代价也很真实——所有 as_i64() 类访问都要走字符串解析(src/number.rs:146 的 self.n.parse().ok())。这个 feature 在高精度金融 / 科学计算场景必选、在普通 Web API场景就是多余开销。一个 type alias 分支把两种完全不同的存储策略压进同一个 API——用户代码不变、只改 Cargo.toml。

这三个细节(三态代数存储 / Hash 归一 / arbitrary_precision feature 切换)合在一起是 "JSON Number 在 Rust 里怎么做才正确"的完整答案——每一条都对应一个容易忽略但影响正确性或表达力的具体问题。

17.10 数值工程:lexical 浮点数解析库

serde_json 源码里有一个 lexical/ 目录(实测 3708 行、19 个 .rs 文件)。它是什么?

先澄清方向:它是解析方向(十进制文本 → f64)的引擎,不是格式化方向。src/lexical/mod.rs:11 的模块文档只有一句:"Fast, minimal float-parsing algorithm."——按文件头注释,它是 lexical crate 的作者为 serde_json 裁剪出来的一个最小子集。

输出方向(f64 → 文本)由 Formatter::write_f64 交给独立的 zmij crate(src/ser.rs:1716-1723,zmij::Buffer::new().format_finite(value);1.0.147 之前用的是 ryu),整数则交给 itoa。

还有一个容易漏掉的事实:lexical 只在开启 float_roundtrip feature 时才编译(src/lib.rs:435-436:#[cfg(feature = "float_roundtrip")] mod lexical;)。这个 feature 默认关闭:

  • 默认路径(src/de.rs:638 的 f64_from_parts):用有效数字乘以预计算的 10 的幂做 "best-effort" 转换,Cargo.toml 明说它"不保证 f64 → JSON → f64 往返一致"。
  • float_roundtrip 路径(src/de.rs:623 的 f64_from_parts):走 lexical::parse_concise_float——先尝试 algorithm.rs 的 fast path(尾数和指数都能精确表示时直接算),再试同文件的 moderate_path(扩展精度近似),仍无法确定舍入方向时才用 bhcomp.rs / math.rs 的大整数比较把结果正确舍入到最近的 f64。large_powers64.rs / large_powers32.rs 里的表就是给这条慢路径用的 5 的幂(src/lexical/math.rs:392 取 large_powers::POW5)。

IEEE 754 浮点数的正确解析并不简单——"把十进制串舍入到最近的二进制浮点数"在最坏情况下需要任意精度算术。serde_json 把这套东西以 feature 形式提供:想要 round-trip 精确的用户付出编译体积和浮点解析开销(Cargo.toml 注释的说法是浮点解析慢约 2 倍),不需要的用户零成本。

这 3708 行代码首先是正确性工程:开启它换来的是往返精确,不是更快的解析。它内部仍按 fast path → moderate path → 大整数比较分层,让常见输入走便宜的路径。至于浮点解析是否是热点,取决于数据:浮点字段密集的 JSON 里它会在关键路径上;字符串或对象结构占主导时,瓶颈会转向转义、map 遍历或写入器。

启示:一个好的格式实现需要的不只是"实现 Serde trait"——还要工业级的底层工程。数值解析与格式化、字符串转义、错误定位(行列号)、内存分配策略——每一处都是性能/质量的关键。

17.11 to_value / from_value:中间类型的双向转换

serde_json 提供一个特别有用的函数对:

// 签名示意(不是源码原文);真身见 src/value/mod.rs:995 与 :1037
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:

// 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, len: Option<usize>) -> Result<SerializeVec> {
        Ok(SerializeVec { vec: Vec::with_capacity(len.unwrap_or(0)) })
    }
    // ...
}

注意 type Ok = Value——Serializer 的 Ok 是返回值!不是 ()(写入副作用)。这是第 3 章提到过的"构建式 Serializer"——用 type Ok 传递返回值。

用途:任何 Serialize 类型都可以转成 Value 做运行时处理:

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:

use serde_json::json;

let v = json!({
    "status": "ok",
    "code": 200,
    "data": {
        "id": 1,
        "tags": ["a", "b", "c"]
    }
});
// v: Value

实现在 src/macros.rs:54(整个文件 303 行声明宏,json! 之外还有 json_internal! 等四个内部宏,见 src/macros.rs:70、:287、:295、:301)——第 5 章讲过这是声明宏而不是过程宏。它递归匹配 JSON-like 语法,展开成 Value::Object / Value::Array 等构造调用。

声明宏的典型应用——不需要 AST 操作,只是"模板化构造"。对这种场景,声明宏足够用,且编译快得多。

17.13 serde_json 整 crate 18276 行的真实尺寸表

把整个 serde-rs/json 仓库 src/ 按文件大小排序——

文件 行 角色
src/de.rs 2704 Deserializer + 字符串转义 + 数字解析 + 错误恢复(§17.6 主角)
src/ser.rs 2285 Serializer + 7 个 SerializeXxx trait 共用的 Compound + Formatter trait(§17.4 主角)
src/value/de.rs 1507 Value 的 Deserialize(JSON → Value)+ Value / &Value 作为 Deserializer(Value → T)
src/map.rs 1181 serde_json::Map——BTreeMap / IndexMap 的 wrapper(feature 切换)
src/read.rs 1089 StrRead / SliceRead / IoRead 三套 Read trait(§17.7)
src/value/ser.rs 1063 T → Value 序列化
src/value/mod.rs 1042 Value enum 定义 + 各种 as_x() 访问器
src/lexical/math.rs 884 大整数运算(浮点解析慢路径的内部表示,仅 float_roundtrip feature)
src/number.rs 814 Number 类型(§17.9.1)
src/raw.rs 784 RawValue——延迟解析的 newtype(用于子 JSON 透传)
src/lexical/large_powers64.rs 625 预计算的 5 的幂表(64-bit limb)
src/error.rs 541 Error 类型 + 行列号定位
src/lib.rs 441 crate 文档 + from_str / to_string / to_value 等顶层 API 的 re-export
src/lexical/num.rs 421 数字 trait 抽象
src/macros.rs 303 json! 宏的 token tree 递归实现
src/value/from.rs 284 From<i32> for Value 等二十多个 impl(整数类型由 from_integer! 宏一次生成 10 个)
src/value/index.rs 258 Index trait 让 value["a"]["b"] 可写
其余 lexical 16 文件 9~231 rounding / bhcomp / cached_float80 / algorithm / float / large_powers32 / errors 等
src/io/core.rs、iter.rs、value/partial_eq.rs 等小文件 ≤ 103 no_std 兼容 + 工具
总计 18276

四条值得记住的物理事实——

  1. de.rs > ser.rs(2704 vs 2285)——反序列化比序列化大 18%——根因是反序列化要做两件序列化没有的事:(a) 字节流的字符级状态机(跳空白、识别字面量、字符串转义);(b) 错误定位(Deserializer 的 fix_position / peek_error 把 Read::position 换算成行列号、error.rs 的 ErrorCode 枚举列出 25 种失败原因)
  2. src/value/de.rs(1507)比 src/value/ser.rs(1063)大 42%——主要因为 Value → T 这一侧写了两遍:impl Deserializer for Value(src/value/de.rs:282)和 impl Deserializer for &'de Value(:817),各配一套 Enum / Variant / Seq / Map 访问器;同一文件还放着 JSON → Value 的 impl Deserialize for Value(:22)和 map key 用的 MapKeyDeserializer。T → Value 一侧只有一个 Serializer(src/value/ser.rs:58),外加 map key 与 Number / RawValue 的发射器
  3. lexical 子目录 3708 行 = 整 crate 20%——纯粹用于浮点解析、而且只在 float_roundtrip feature 下编译(§17.10)——其中 large_powers64.rs 625 + large_powers32.rs 183 = 808 行只是预计算的 5 的幂查表——为了让慢路径的大整数比较不必在运行时重复算幂
  4. raw.rs 单独 784 行——RawValue 是个看似简单的 str newtype、但要正确实现 Serialize/Deserialize 需要和 serde_json 自己的 Serializer/Deserializer 用一个私有名字握手:src/raw.rs:299 的 TOKEN = "$serde_json::private::RawValue" 作为 struct 名传给 serialize_struct / deserialize_newtype_struct;serde_json 自己的 Serializer(src/ser.rs:377)和 Deserializer(src/de.rs:1728、2284)认出这个名字就切换到"原样透传"路径;换成别的 Serializer 它就退化成普通 struct。serde 主仓库不需要知道 RawValue 的存在

17.14 三个容易忽视的格式边界:Map、RawValue、StreamDeserializer

理解 serde_json 不能只看 from_str / to_string。真实项目里经常踩坑的,是三个边界类型:对象 map、原始 JSON 片段、连续 JSON 流。

第一,Map<String, Value> 默认不是插入有序。 src/map.rs:1-4 的模块注释写明:默认 backing 是 BTreeMap,开启 preserve_order feature 才改用 IndexMap。类型别名在 src/map.rs:33-36 落地:未开启 feature 时 MapImpl<K, V> = BTreeMap<K, V>,开启时是 IndexMap<K, V>。

这影响所有依赖 Value::Object 的代码。你如果把 JSON 当"有序配置文件"处理,默认 serde_json::Value 会按 key 排序,而不是保留输入顺序。想保留顺序,要在依赖里显式开启 feature。这个行为不是文档角落里的小字,而是 map 类型的源码结构决定的。

src/map.rs:490-503 给 Map<String, Value> 实现 Serialize,内部调用 serializer.serialize_map(Some(self.len())) 并逐项 serialize_entry;src/map.rs:505-547 给它实现 Deserialize,用 Visitor 的 visit_map 循环填充新 map。src/map.rs:601-615 还为 owned 和 borrowed map 实现 IntoDeserializer,这让内存里的 Value::Object 可以再次作为反序列化输入,支撑 from_value 这类 API。

第二,RawValue 是"延迟解析"边界,不是普通字符串。 它需要开启 raw_value feature(src/lib.rs:440-441)。src/raw.rs:18-27 说明它用于推迟解析,或者完全不解析、只把一段 payload 原样转发;序列化时保留原始格式,不会被 minify 或 pretty-print。src/raw.rs:81-100 给出两个形态:从 from_str / from_slice 进入时可以借用 &RawValue,从 from_reader 进入时必须用 boxed 形态,因为 I/O 流需要把原始片段缓冲到内存。

src/raw.rs:116-132 把 RawValue 定义成 repr(transparent) 的 str newtype,并用受控的 transmute 在 &str / Box<str> 和 RawValue 之间转换。这段实现说明 RawValue 的语义是"这段字符串已经被验证为 JSON 值",不是"任意字符串以后再说"。它适合网关、日志管道、签名透传这类场景:外层结构由 Serde 检查,内层大 JSON 片段保持原貌。

第三,StreamDeserializer 是连续值协议的边界。 src/de.rs:2330-2356 定义 StreamDeserializer,文档示例展示同一输入里连续放对象、数字、字符串、对象和数组;src/de.rs:2434-2450 的 Iterator::next 每次跳过空白后反序列化一个 T。这不是 JSON array,而是"多个自分界 JSON 值连续排列"。

flowchart TD
  A[serde_json 格式边界] --> B[Map: 对象存储顺序]
  A --> C[RawValue: 原始片段透传]
  A --> D[StreamDeserializer: 连续 JSON 值]
  B --> E[BTreeMap 或 IndexMap]
  C --> F[借用或 boxed]
  D --> G[Iterator<Item = Result<T>>]

这三个边界对应三类工程决策:

决策 默认行为 什么时候要显式调整
对象 key 顺序 BTreeMap 排序 需要保留输入顺序时启用 preserve_order
子 JSON 是否解析 普通字段会完整解析 只透传或延迟处理时用 RawValue
输入是否只有一个值 from_str 期待一个完整值 日志流、拼接响应、增量协议用 StreamDeserializer

把这些边界纳入设计,比在业务层补丁式处理更稳。比如 LLM 工具调用返回一串 JSON 片段时,不应先手写 split;应该用 StreamDeserializer 判断每个片段是否是完整 JSON 值。API 网关需要转发未知字段时,不应把大对象解析成 Value 再写回;如果只需要原样透传,可以考虑 RawValue。配置文件需要保留用户书写顺序时,不应假设 Value::Object 默认有序,而要明确 feature 选择。

还有一个与第 3 章呼应的边界:JSON map key。src/ser.rs:2238-2245 的 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,哪些复杂度来自格式本身。

17.15 和其他 Serde 格式 crate 的对比

知道 serde_json 的实现后,看其他格式 crate 会更快:

  • bincode:二进制紧凑格式。它的 Serializer 没有 Formatter 层——直接按二进制布局写。不支持 deserialize_any(非自描述)。
  • rmp-serde(MessagePack):二进制自描述格式。struct 可以按数组(省字段名)或按 map(带字段名)编码,由序列化器配置决定。支持 deserialize_any。
  • serde_yaml:0.9 起底层换成 unsafe-libyaml(libyaml 的 Rust 移植);仓库 2024 年已归档不再维护。YAML 语法复杂度远超 JSON,实现体量也大得多。
  • toml:TOML 格式。实现思路不同——按 TOML 1.0 规范,[table] 表头为其后直到下一个表头(或文件末尾)的键值对设定所属的表,而且规范允许乱序定义表(例如先写 [x.y.z.w] 再补 [x],只是不推荐);同一张表的内容可能分散在文档各处,所以反序列化前一般要先读完整份文档、组装出表结构,不能像 serde_json 那样边读边分派。
  • postcard:极简二进制格式,针对嵌入式。高度优化、no_std 优先。

所有 serde 格式 crate 共享同一套 Serde trait——用户只需学一次 API 就能切换任何格式。这是第 1 章讲的 M+N 架构的实际威力。

17.16 和本系列其他专栏的关联

serde_json 和本系列里多个专栏有深度关联:

  • 《MCP 协议设计与实现》专栏第 3 章"JSON-RPC 与消息格式":那一章以 TypeScript / Python SDK 讲 JSON-RPC 2.0 的消息格式;在 Rust 里处理同样的消息,编解码就落在本章讲的 Serializer/Deserializer 上。
  • 《Tokio 源码深度解析》专栏第 17 章 "可观测性":tracing 输出 JSON 日志时调用 serde_json——高频调用场景下 serde_json 的性能设计(Formatter 分离、itoa / zmij 数字格式化、optimistic borrowing)是 JSON 日志层开销可控的基础。
  • 《Rust 编译器与运行时揭秘》专栏第 7 章"Trait 静态分发":serde_json Serializer 的每个方法都是泛型/单态化——编译器为每种"数据类型 × Serializer" 组合生成单独代码。这就是该专栏第 6 章"单态化"的实际应用。

17.17 本章小结

serde_json 是 Serde 生态"格式实现"的参考标杆。本章讲了它的核心设计:

  1. 分层架构:Serializer(语义层)+ Formatter(格式层)——解耦"Data Model 转换"和"字节输出风格"。
  2. Compound 状态机:7 个 SerializeXxx 关联类型全部是 Compound——通过 State enum 管理进度,极大减少代码量。
  3. 手写 JSON 解析器:de.rs 2700+ 行手写字符、状态机、错误定位。deserialize_any 是主引擎,其他 deserialize 方法做类型特定优化。
  4. Read trait 的输入抽象:StrRead / SliceRead / IoRead 三种实现。StrRead 和 SliceRead 支持 Reference::Borrowed 零拷贝,IoRead 必须复制——这是第 15 章借用反序列化的底层支撑。
  5. Value 动态树:用 enum + DeserializeAny 支持运行时 JSON 操作。
  6. 数值工程:lexical 浮点解析库 3708 行 19 文件(float_roundtrip feature 下编译)专门保证浮点解析的往返精确,格式化交给 itoa / zmij——说明"格式实现"不只是 trait 实现,还有工业级底层工程。

serde_json 整 crate 18276 行——de.rs (2704) + ser.rs (2285) + lexical/ (3708) 三块占 48%——是理解整个 JSON 实现的最小必读子集。

动手实验

  1. 测试 pretty 和 compact 的区别:用 serde_json::to_string_pretty 和 to_string 序列化同一个对象,观察输出差异。然后读 ser.rs 的 PrettyFormatter 实现,看它如何添加换行和缩进。
  2. 手写一个 Formatter:实现一个 "YAML-like Formatter",让 serde_json 输出 YAML 风格的 JSON。不需要完整,实现 begin_object/end_object/begin_object_key 等几个方法就好。
  3. 测试 StrRead 和 IoRead 的性能差异:用同一份 JSON,一次 from_str、一次 from_reader(用 Cursor 包装同样的字节)。用 criterion 对比时间。注意不同的 struct(有 &str 字段 vs 全 String 字段)。
  4. 读 lexical/:开着 float_roundtrip feature 打开浮点解析库,沿 parse_concise_float 看 fast path 何时够用、何时退到大整数比较。这是一个独立的小引擎,读懂它你能理解"为什么把十进制串正确舍入成 f64 是个非平凡问题"。

延伸阅读

  • serde_json 仓库:完整源码。2700 行的 de.rs 是整个仓库最有"工程感"的一份代码。
  • JSON RFC 8259:JSON 格式规范,读懂 serde_json 对各种边缘情况(空对象、嵌套深度限制、Unicode 处理)的处理选择。
  • Ryū 浮点格式化论文:"最短往返"浮点格式化的经典算法;serde_json 1.0.147 之前用的 ryu crate 就是它的实现,之后换成了同类的 zmij。深度内容,但对性能工程师必读。
  • Other Serde formats:bincode、postcard、rmp-serde、serde_yaml 等格式 crate。
  • 《MCP 协议设计与实现》专栏第 3 章:看 JSON-RPC 2.0 消息格式本身的设计。
  • 《Rust 编译器与运行时揭秘》专栏第 7 章:"Trait 静态分发" 解释为什么 serde_json 的泛型方法能零成本。