序列化一侧
主线位置:序列化主线的格式那一端。一次
to_string里,derive 生成的Serialize::serialize每调一次serialize_struct、serialize_field、end,serde_json 就当场把对应的字节写出去。本页讲接住这些调用的几样东西:Serializer<W, F>、状态对象Compound、决定标点的Formatter、字符串转义、数字格式化、map 键的MapKeySerializer,以及字节最后怎么到达io::Write。
flowchart TB ts["to_string"] --> tv["to_vec:Vec 预留 128"] tv --> tw["to_writer"] tw --> ser["Serializer:writer 加 formatter"] ser --> cmp["Compound:ser 和 State"] cmp --> key[MapKeySerializer] cmp --> fmtnode["Formatter 钩子"] key --> fmtnode fmtnode --> esc["转义表、itoa、zmij"] esc --> io["io::Write 的 write_all"]
一次往返已经用一个打印调用的包装器把 Post 的序列化逐步走过一遍:serialize_struct 转给 serialize_map 写 {,serialize_field 转给 serialize_entry,end 写 }。Serializer trait 本身的约定(按值拿 self、七个状态对象、len 只是声明)在 Serialize 与 Serializer。这一页不再走那条线,而是把 serde_json ser.rs(2285 行)里那条线经过的部件逐个拆开。反方向的读取在反序列化一侧,内存里的 Value 在 Value。
本页的实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154",没有打开 serde_json 的任何可选 feature。
Serializer<W, F>:一个 writer 加一个 Formatter
Serializer 只有两个字段:
pub struct Serializer<W, F = CompactFormatter> {
writer: W,
formatter: F,
}
writer 是输出的去处,formatter 决定 JSON 的标点长什么样。三个构造函数只是换 F:new 用 CompactFormatter,pretty 用 PrettyFormatter::new(),with_formatter 收一个自定义的;into_inner 把 writer 交还。F 带默认值,所以 Serializer<Vec<u8>> 就是紧凑输出。
实现 serde::Serializer 的是 &'a mut Serializer<W, F>,不是 Serializer<W, F>。为什么要给可变引用实现、递归时怎么重借用,在 Serialize 与 Serializer 讲过。这里只看关联类型:
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>;
Ok = ():输出已经写进 writer,没有东西要返回。七个状态对象是同一个类型 Compound,它借着 'a 这次可变借用,在 end 之前一直占着 Serializer。
数据模型到 JSON 的映射
30 个 serialize_* 方法里,标量方法都是「交给 formatter 的某个 write_*,再把 io::Error 包成 serde_json::Error」。合起来是这张表:
| 数据模型 | 方法 | 写出 |
|---|---|---|
| bool | serialize_bool | true / false |
| i8 … i128、u8 … u128 | serialize_i64 等 | 十进制整数(itoa),i128 / u128 也直接支持 |
| f32、f64 | serialize_f64 | 有限值用 zmij;NaN 和无穷写成 null |
| char | serialize_char | 编码成 UTF-8 后按字符串写 |
| str | serialize_str | 带转义的字符串 |
| bytes | serialize_bytes | 整数数组(Formatter::write_byte_array) |
| unit、unit_struct、none | serialize_unit | null |
| some、newtype_struct | serialize_some、serialize_newtype_struct | 里面的值,不加包装 |
| unit_variant | serialize_unit_variant | 变体名字符串 "V" |
| newtype_variant | serialize_newtype_variant | {"V":值} |
| seq、tuple、tuple_struct | serialize_seq | [...] |
| tuple_variant | serialize_tuple_variant | {"V":[...]} |
| map、struct | serialize_map、serialize_struct | {...} |
| struct_variant | serialize_struct_variant | {"V":{...}} |
serialize_struct 不看名字,直接转给 serialize_map(Some(len)),只有两个例外:打开 arbitrary_precision 时名字是 $serde_json::private::Number、打开 raw_value 时名字是 $serde_json::private::RawValue,这两个魔法名字让 Number 和 RawValue 把一段已经格式化好的文本原样写出,在 Value 讲。
几个映射有信息损失:Some(x) 和 x 写出来一样,() 和 None 都是 null,NaN 也是 null。NaN 写成 null 不报错,但读回 f64 会失败:
fn main() {
let text = serde_json::to_string(&vec![1.5f64, f64::NAN, f64::INFINITY]).unwrap();
println!("to_string {text}");
let back: Result<Vec<f64>, _> = serde_json::from_str(&text);
println!("from_str f64 {:?}", back.map_err(|e| e.to_string()));
let opt: Vec<Option<f64>> = serde_json::from_str(&text).unwrap();
println!("from_str Option {opt:?}");
}
to_string [1.5,null,null]
from_str f64 Err("invalid type: null, expected f64 at line 1 column 9")
from_str Option [Some(1.5), None, None]
Compound 与 State:逗号写在哪里
// Not public API. Should be pub(crate).
#[doc(hidden)]
#[derive(Eq, PartialEq)]
pub enum State {
Empty,
First,
Rest,
}
// Not public API. Should be pub(crate).
#[doc(hidden)]
pub enum Compound<'a, W: 'a, F: 'a> {
Map {
ser: &'a mut Serializer<W, F>,
state: State,
},
#[cfg(feature = "arbitrary_precision")]
Number { ser: &'a mut Serializer<W, F> },
#[cfg(feature = "raw_value")]
RawValue { ser: &'a mut Serializer<W, F> },
}
不开 feature 时 Compound 只有一个变体 Map,数组也用它,名字有点误导。它装着两样东西:对 Serializer 的借用,和「这一层容器已经写到哪一步」的 State。
First:开始符已写,还没有元素。下一个元素前不写逗号。Rest:至少写过一个元素。下一个元素前写逗号。Empty:len == Some(0),开始符和结束符在serialize_seq/serialize_map里一次写完,end什么也不写。
SerializeSeq::serialize_element 和 end:
#[inline]
fn serialize_element<T>(&mut self, value: &T) -> Result<()>
where
T: ?Sized + Serialize,
{
match self {
Compound::Map { ser, state } => {
tri!(ser
.formatter
.begin_array_value(&mut ser.writer, *state == State::First)
.map_err(Error::io));
*state = State::Rest;
tri!(value.serialize(&mut **ser));
ser.formatter
.end_array_value(&mut ser.writer)
.map_err(Error::io)
}
// ...
}
}
#[inline]
fn end(self) -> Result<()> {
match self {
Compound::Map { ser, state } => match state {
State::Empty => Ok(()),
_ => ser.formatter.end_array(&mut ser.writer).map_err(Error::io),
},
// ...
}
}
Compound 自己不写逗号,它只算出一个 first: bool 交给 Formatter,写不写、写什么由 Formatter 决定。分工的理由在于状态放在哪里:每一层容器有自己的 Compound,[[1,2],[3]] 序列化时外层和内层各有一个,各自记着自己的 State,嵌套多深都不会混;Formatter 只有一个,被所有层共用,它不需要知道「我现在在哪一层的第几个元素」,因为调用方每次都告诉它。所以 CompactFormatter 可以是一个没有字段的单元结构体。
State 是公开的但标着 #[doc(hidden)],注释直说「Should be pub(crate)」:Compound 作为公开 trait 的关联类型出现在签名里,必须是 pub,它的字段类型也就跟着 pub。
len 的 Some(0) 特判如果被一个撒谎的 Serialize 实现利用(声明 Some(0) 却写入元素),会写出 [],1,2],to_string_pretty 在 debug 下 panic、release 下停不下来,实验在 Serialize 与 Serializer 的「len:声明,不是约束」一节。
带变体名的两种复合类型
serialize_tuple_variant 和 serialize_struct_variant 先写 {、变体名和 :,再调 serialize_seq / serialize_map 开里面那层。于是 end 要关两层,SerializeStructVariant::end:
#[inline]
fn end(self) -> Result<()> {
match self {
Compound::Map { ser, state } => {
match state {
State::Empty => {}
_ => tri!(ser.formatter.end_object(&mut ser.writer).map_err(Error::io)),
}
tri!(ser
.formatter
.end_object_value(&mut ser.writer)
.map_err(Error::io));
ser.formatter.end_object(&mut ser.writer).map_err(Error::io)
}
// ...
}
}
里面那层按 State 决定要不要写 }(Empty 时已经写过),外面那层的 } 总是要写。外层不需要单独的 Compound:它只有一个键,first 永远是 true,所以 serialize_struct_variant 直接传了字面量 begin_object_key(&mut self.writer, true)。
Formatter:把标点交出去
Formatter 的文档是「This trait abstracts away serializing the JSON control characters, which allows the user to optionally pretty print the JSON output.」它的方法全都有默认实现,默认实现就是紧凑输出,所以 CompactFormatter 只有一行:
impl Formatter for CompactFormatter {}
方法分四组,每个都拿 &mut self 和 writer: &mut W(W: ?Sized + io::Write),返回 io::Result<()>:
| 组 | 方法 | 默认写出 |
|---|---|---|
| 值 | write_null、write_bool、write_i8 … write_u128、write_f32、write_f64、write_number_str、write_raw_fragment、write_byte_array | 值本身 |
| 字符串 | begin_string、end_string、write_string_fragment、write_char_escape | "、"、原文、\n 这类转义 |
| 数组 | begin_array、end_array、begin_array_value(first)、end_array_value | [、]、非首个元素前的 ,、无 |
| 对象 | begin_object、end_object、begin_object_key(first)、end_object_key、begin_object_value、end_object_value | {、}、非首个键前的 ,、无、:、无 |
end_array_value、end_object_key、end_object_value 默认什么也不写,它们是给需要记账的 Formatter 留的钩子(下面的 PrettyFormatter 就用 end_*_value 记账)。
写一个 Formatter 包住 CompactFormatter,每个钩子先打印再转发,就能看到一次序列化里钩子被调用的顺序。src/bin/hooks.rs:
use serde::Serialize;
use serde_json::ser::{CompactFormatter, Formatter, Serializer};
use std::io::{self, Write};
#[derive(Serialize)]
struct Post {
title: String,
tags: Vec<&'static str>,
draft: Option<bool>,
}
// Logs each Formatter hook, then lets CompactFormatter write the bytes.
struct Log(CompactFormatter);
macro_rules! hook {
($name:ident) => {
fn $name<W: ?Sized + Write>(&mut self, w: &mut W) -> io::Result<()> {
println!("{}", stringify!($name));
self.0.$name(w)
}
};
($name:ident, first) => {
fn $name<W: ?Sized + Write>(&mut self, w: &mut W, first: bool) -> io::Result<()> {
println!("{}(first={first})", stringify!($name));
self.0.$name(w, first)
}
};
}
impl Formatter for Log {
hook!(begin_object);
hook!(end_object);
hook!(begin_object_key, first);
hook!(end_object_key);
hook!(begin_object_value);
hook!(end_object_value);
hook!(begin_array);
hook!(end_array);
hook!(begin_array_value, first);
hook!(end_array_value);
hook!(begin_string);
hook!(end_string);
hook!(write_null);
fn write_string_fragment<W: ?Sized + Write>(&mut self, w: &mut W, s: &str) -> io::Result<()> {
println!("write_string_fragment({s:?})");
self.0.write_string_fragment(w, s)
}
}
fn main() {
let post = Post { title: "serde".into(), tags: vec!["rust", "json"], draft: None };
let mut out = Vec::new();
post.serialize(&mut Serializer::with_formatter(&mut out, Log(CompactFormatter))).unwrap();
println!("=> {}", String::from_utf8(out).unwrap());
println!("pretty =>\n{}", serde_json::to_string_pretty(&post).unwrap());
let mut out = Vec::new();
let fmt = serde_json::ser::PrettyFormatter::with_indent(b"\t");
post.serialize(&mut Serializer::with_formatter(&mut out, fmt)).unwrap();
println!("tab => {:?}", String::from_utf8(out).unwrap());
let empty: (Vec<u8>, std::collections::BTreeMap<String, u8>) = (vec![], Default::default());
println!("empty => {}", serde_json::to_string_pretty(&empty).unwrap());
}
begin_object
begin_object_key(first=true)
begin_string
write_string_fragment("title")
end_string
end_object_key
begin_object_value
begin_string
write_string_fragment("serde")
end_string
end_object_value
begin_object_key(first=false)
begin_string
write_string_fragment("tags")
end_string
end_object_key
begin_object_value
begin_array
begin_array_value(first=true)
begin_string
write_string_fragment("rust")
end_string
end_array_value
begin_array_value(first=false)
begin_string
write_string_fragment("json")
end_string
end_array_value
end_array
end_object_value
begin_object_key(first=false)
begin_string
write_string_fragment("draft")
end_string
end_object_key
begin_object_value
write_null
end_object_value
end_object
=> {"title":"serde","tags":["rust","json"],"draft":null}
pretty =>
{
"title": "serde",
"tags": [
"rust",
"json"
],
"draft": null
}
tab => "{\n\t\"title\": \"serde\",\n\t\"tags\": [\n\t\t\"rust\",\n\t\t\"json\"\n\t],\n\t\"draft\": null\n}"
empty => [
[],
{}
]
每个键都是 begin_object_key → 字符串三件套 → end_object_key,每个值都夹在 begin_object_value / end_object_value 之间;first 只在每层第一个键或元素上是 true,正是上一节 State::First 算出来的。draft: None 走 serialize_none → serialize_unit → write_null。
PrettyFormatter:一个深度计数加一个布尔
PrettyFormatter 有三个字段:
pub struct PrettyFormatter<'a> {
current_indent: usize,
has_value: bool,
indent: &'a [u8],
}
indent 是一层缩进的字节,new() 用 b" "(两个空格),with_indent 可以换成别的(上面的实验用了 b"\t")。它覆盖了数组和对象的钩子,数组一侧:
#[inline]
fn begin_array<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.current_indent += 1;
self.has_value = false;
writer.write_all(b"[")
}
#[inline]
fn end_array<W>(&mut self, writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.current_indent -= 1;
if self.has_value {
tri!(writer.write_all(b"\n"));
tri!(indent(writer, self.current_indent, self.indent));
}
writer.write_all(b"]")
}
#[inline]
fn begin_array_value<W>(&mut self, writer: &mut W, first: bool) -> io::Result<()>
where
W: ?Sized + io::Write,
{
tri!(writer.write_all(if first { b"\n" } else { b",\n" }));
indent(writer, self.current_indent, self.indent)
}
#[inline]
fn end_array_value<W>(&mut self, _writer: &mut W) -> io::Result<()>
where
W: ?Sized + io::Write,
{
self.has_value = true;
Ok(())
}
对象一侧一样,另外把 begin_object_value 从 : 改成 : 。
current_indent 是当前深度,开始符加一、结束符减一,每个元素前写 current_indent 次 indent(indent 就是一个循环)。has_value 只有一个布尔,却要回答「这一层有没有写过元素」:开始符把它清成 false,每个元素写完由 end_*_value 置成 true。内层容器结束时会改掉它,但内层本身就是外层的一个元素,紧接着外层的 end_*_value 又把它置成 true,所以轮到外层的结束符时,它看到的值总是对的。空容器的开始符和结束符之间没有任何元素,has_value 停在 false,于是写成 []、{} 而不是 [\n],就是上面 empty => 那行。
current_indent -= 1 是普通的 usize 减法:结束符多于开始符时,debug 下溢出 panic。正常的 Serialize 实现不会这样,只有撒谎的 len 才会(见上文链接)。
问: to_string 用 String::from_utf8_unchecked 把字节直接当成字符串,可 Formatter 能自己实现,一个自定义的 write_string_fragment 写出非 UTF-8 的字节怎么办?
答: to_string 和 to_string_pretty 根本不收 Formatter 参数,它们内部只用 CompactFormatter 和 PrettyFormatter,这两个写出的都是 ASCII 标点、itoa / zmij 的数字和原样的 &str 片段,from_utf8_unchecked 的前提(注释写的「We do not emit invalid UTF-8.」)只需要对这两个成立。想用自定义 Formatter,就只能像上面实验那样 Serializer::with_formatter 写进自己的 Vec<u8>,转字符串时用的是你自己选的 String::from_utf8,检查不会被跳过。to_writer 的文档写的「Serialization guarantees it only feeds valid UTF-8 sequences to the writer」说的也是 new / pretty 这两种。
字符串转义:一张 256 字节的表
serialize_str 调 format_escaped_str:begin_string、内容、end_string。内容由 format_escaped_str_contents 写,它查的是 ESCAPE 表:
const BB: u8 = b'b'; // \x08
const TT: u8 = b't'; // \x09
const NN: u8 = b'n'; // \x0A
const FF: u8 = b'f'; // \x0C
const RR: u8 = b'r'; // \x0D
const QU: u8 = b'"'; // \x22
const BS: u8 = b'\\'; // \x5C
const UU: u8 = b'u'; // \x00...\x1F except the ones above
const __: u8 = 0;
// Lookup table of escape sequences. A value of b'x' at index i means that byte
// i is escaped as "\x" in JSON. A value of 0 means that byte i is not escaped.
static ESCAPE: [u8; 256] = [
// 1 2 3 4 5 6 7 8 9 A B C D E F
UU, UU, UU, UU, UU, UU, UU, UU, BB, TT, NN, UU, FF, RR, UU, UU, // 0
UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, UU, // 1
__, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2
// ...
__, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5
// ...
__, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F
];
表里非零的只有 34 项:0x00–0x1F 这 32 个控制字符、"(0x22)和 \(0x5C)。这正是 JSON 规范要求必须转义的字符。表项的值就是反斜杠后面那个字母,UU 表示写成 \u00XX。循环本身:
let mut bytes = value.as_bytes();
let mut i = 0;
while i < bytes.len() {
let (string_run, rest) = bytes.split_at(i);
let (&byte, rest) = rest.split_first().unwrap();
let escape = ESCAPE[byte as usize];
i += 1;
if escape == 0 {
continue;
}
bytes = rest;
i = 0;
// Safety: string_run is a valid utf8 string, since we only split on ascii sequences
let string_run = unsafe { str::from_utf8_unchecked(string_run) };
if !string_run.is_empty() {
tri!(formatter.write_string_fragment(writer, string_run));
}
let char_escape = match escape {
self::BB => CharEscape::Backspace,
// ...
self::UU => CharEscape::AsciiControl(byte),
// Safety: the escape table does not contain any other type of character.
_ => unsafe { hint::unreachable_unchecked() },
};
tri!(formatter.write_char_escape(writer, char_escape));
}
// Safety: bytes is a valid utf8 string, since we only split on ascii sequences
let string_run = unsafe { str::from_utf8_unchecked(bytes) };
if string_run.is_empty() {
return Ok(());
}
formatter.write_string_fragment(writer, string_run)
逐字节查表,遇到不需要转义的字节只是 i += 1;遇到要转义的,先把前面攒下的一整段用一次 write_string_fragment 写出,再写转义序列,然后从转义字符之后重新开始。一个没有任何转义的字符串只调一次 write_string_fragment,前面 hooks.rs 的输出里每个字符串都只有一行 write_string_fragment 就是这样。
两处 from_utf8_unchecked 的理由写在注释里:切分点只落在需要转义的字节上,这些字节都小于 0x80,而 UTF-8 多字节字符的每个字节都不小于 0x80,所以切分永远不会落在一个字符中间,两边都还是合法的 UTF-8。同一个事实也说明了为什么表只需要按字节查:非 ASCII 字符整段原样写出,不需要解码。
CharEscape 里有一个 Solidus(/),write_char_escape 也会把它写成 \/,但 ESCAPE 表里 0x2F 是 __,format_escaped_str_contents 永远不会产生它:它只是留给自定义 Formatter 的。下面这个程序把几类字符和后面要讲的数字放在一起看,src/bin/scalars.rs:
use serde_bytes_like::Bytes;
mod serde_bytes_like {
// A &[u8] that calls serialize_bytes (Vec<u8> would call serialize_seq).
pub struct Bytes<'a>(pub &'a [u8]);
impl serde::Serialize for Bytes<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_bytes(self.0)
}
}
}
fn show<T: serde::Serialize + ?Sized>(label: &str, v: &T) {
let json = serde_json::to_string(v).unwrap();
// Non-ASCII output bytes are shown as Rust escapes so they are visible.
let shown: String = json.chars().map(|c| if c.is_ascii() { c.to_string() } else { c.escape_unicode().to_string() }).collect();
let shown = shown.replace('\x7f', "<DEL>");
println!("{label:<22} {shown} ({} bytes)", json.len());
}
fn main() {
show("quote backslash", "a\"b\\c");
show("slash", "</script>");
show("\\n \\t \\r \\x08 \\x0c", "\n\t\r\u{8}\u{c}");
show("\\x01 \\x1f", "\u{1}\u{1f}");
show("\\x7f (DEL)", "\u{7f}");
show("non-ASCII", "é中😀");
show("U+2028 (LS)", "\u{2028}");
show("char '\\0'", &'\0');
show("f64 1.0", &1.0f64);
show("f64 0.1+0.2", &(0.1f64 + 0.2));
show("f64 1e20", &1e20f64);
show("f64 1e-7", &1e-7f64);
show("f64 -0.0", &-0.0f64);
show("f64 MAX", &f64::MAX);
show("f32 0.1", &0.1f32);
show("f64 from 0.1f32", &(0.1f32 as f64));
show("f64 NaN", &f64::NAN);
show("f64 -inf", &f64::NEG_INFINITY);
show("u64 MAX", &u64::MAX);
show("i128 MIN", &i128::MIN);
show("u128 MAX", &u128::MAX);
show("Vec<u8>", &vec![1u8, 2, 3]);
show("serialize_bytes", &Bytes(&[1, 2, 3]));
show("()", &());
show("Some(()) ", &Some(()));
}
quote backslash "a\"b\\c" (9 bytes)
slash "</script>" (11 bytes)
\n \t \r \x08 \x0c "\n\t\r\b\f" (12 bytes)
\x01 \x1f "\u0001\u001f" (14 bytes)
\x7f (DEL) "<DEL>" (3 bytes)
non-ASCII "\u{e9}\u{4e2d}\u{1f600}" (11 bytes)
U+2028 (LS) "\u{2028}" (5 bytes)
char '\0' "\u0000" (8 bytes)
f64 1.0 1.0 (3 bytes)
f64 0.1+0.2 0.30000000000000004 (19 bytes)
f64 1e20 1e+20 (5 bytes)
f64 1e-7 1e-7 (4 bytes)
f64 -0.0 -0.0 (4 bytes)
f64 MAX 1.7976931348623157e+308 (23 bytes)
f32 0.1 0.1 (3 bytes)
f64 from 0.1f32 0.10000000149011612 (19 bytes)
f64 NaN null (4 bytes)
f64 -inf null (4 bytes)
u64 MAX 18446744073709551615 (20 bytes)
i128 MIN -170141183460469231731687303715884105728 (40 bytes)
u128 MAX 340282366920938463463374607431768211455 (39 bytes)
Vec<u8> [1,2,3] (7 bytes)
serialize_bytes [1,2,3] (7 bytes)
() null (4 bytes)
Some(()) null (4 bytes)
程序把输出里的非 ASCII 字符改用 Rust 的 \u{...} 显示、把 0x7F 显示成 <DEL>,这样看得见;字节数是 serde_json 真正写出的长度。
"和\各加一个反斜杠;\n、\t、\r、0x08、0x0C 用短写法;其余控制字符用\u00XX,十六进制是小写(HEX_DIGITS是b"0123456789abcdef")。</script>原样写出。把 JSON 嵌进 HTML 的<script>里时,这一点要自己处理。- 0x7F(DEL)不在 0x00–0x1F 里,原样写出,3 字节就是两个引号加它本身。
é中😀原样写出 UTF-8,11 字节 = 2 + 2 + 3 + 4。serde_json 没有「只输出 ASCII」的选项。- U+2028(行分隔符)也原样写出(3 字节 UTF-8 加两个引号),JSON 不要求转义它。
unsafe 是 2025 年为了 2–5% 加进去的在 serde-rs/json PR #1273(首次发布于 v1.0.141)之前,这个循环是 for (i, &byte) in bytes.iter().enumerate(),记一个 start,用 &value[start..i] 取出要写的片段;转义字母换成 CharEscape 的那个 match 写在 CharEscape::from_escape_table 里,兜底分支是 _ => unreachable!()。&value[start..i] 是对 str 的切片,每次都要检查切分点在字符边界上;unreachable!() 是一个会 panic 的分支。
现在的代码就是上面那段:在字节切片上 split_at,用 from_utf8_unchecked 跳过边界检查(理由是注释里那句「we only split on ascii sequences」),兜底分支换成 hint::unreachable_unchecked()。
数字:itoa 与 zmij
serde_json 的 Cargo.toml 里格式化数字的两个依赖是 itoa = "1.0" 和 zmij = "1.0"。整数走 itoa,write_u64:
#[inline]
fn write_u64<W>(&mut self, writer: &mut W, value: u64) -> io::Result<()>
where
W: ?Sized + io::Write,
{
let mut buffer = itoa::Buffer::new();
let s = buffer.format(value);
writer.write_all(s.as_bytes())
}
itoa::Buffer 是栈上的定长缓冲,格式化不分配,结果用一次 write_all 写出。i128 / u128 同样走 itoa(write_i128),上面的输出里 i128::MIN 写成了完整的 40 个字符。写出没问题,但读的一方未必接得住:JavaScript 的 JSON.parse 把所有数字读成 64 位浮点。
浮点走 zmij,write_f64:
#[inline]
fn write_f64<W>(&mut self, writer: &mut W, value: f64) -> io::Result<()>
where
W: ?Sized + io::Write,
{
let mut buffer = zmij::Buffer::new();
let s = buffer.format_finite(value);
writer.write_all(s.as_bytes())
}
format_finite 要求输入是有限值,write_f64 的文档写明「This function does not check for NaN or infinity」,检查放在调用它的 serialize_f64:value.classify() 是 FpCategory::Nan | FpCategory::Infinite 就改调 write_null。所以自定义 Formatter 直接调 write_f64 时要自己先检查。
对照上面的输出:
- 输出是能精确读回同一个浮点数的最短十进制串:
0.1 + 0.2写成0.30000000000000004,f64::MAX是 17 位有效数字。 - 整数值的浮点数保留
.0(1.0、-0.0),读回时仍是浮点而不是整数。 - 指数形式带符号:
1e+20、1e-7、1.7976931348623157e+308。 f32按f32的精度求最短串,0.1f32写成0.1;先转成f64再写,就是0.10000000149011612,这是0.1f32在f64里的真实值。
zmij 是 1.0.147 才换上的,之前用 ryu,来历在 错误 一页的冷知识里。
MapKeySerializer:键必须写成字符串
JSON 对象的键只能是字符串,Rust 的 map 键却可以是任何类型。SerializeMap::serialize_key 在写键时把 Serializer 换掉:
Compound::Map { ser, state } => {
tri!(ser
.formatter
.begin_object_key(&mut ser.writer, *state == State::First)
.map_err(Error::io));
*state = State::Rest;
tri!(key.serialize(MapKeySerializer { ser: *ser }));
ser.formatter
.end_object_key(&mut ser.writer)
.map_err(Error::io)
}
MapKeySerializer 包着同一个 &mut Serializer,自己实现了一遍 serde::Serializer,每个方法决定一种数据模型类型能不能当键:
serialize_str转给外层的serialize_str;serialize_char编码后同样处理;serialize_unit_variant写变体名。整数、
bool、f32/f64:begin_string、write_*、end_string,也就是把数字或布尔包上引号。serialize_bool:fn serialize_bool(self, value: bool) -> Result<()> { tri!(self .ser .formatter .begin_string(&mut self.ser.writer) .map_err(Error::io)); tri!(self .ser .formatter .write_bool(&mut self.ser.writer, value) .map_err(Error::io)); self.ser .formatter .end_string(&mut self.ser.writer) .map_err(Error::io) }浮点数多一步:
serialize_f64先查is_finite(),NaN 和无穷报float_key_must_be_finite。作为值的 NaN 会悄悄变成null,作为键却报错,因为null不是字符串,写成"null"又读不回 NaN。serialize_newtype_struct和serialize_some透过去序列化里面的值。其余的(
bytes、unit、unit_struct、none、newtype_variant,以及七种复合类型)全都返回key_must_be_a_string。七个状态对象的关联类型填的是Impossible<(), Error>(Serialize 与 Serializer 讲过它)。
用一个只含一个键的 map 把常见的键类型都试一遍,同时比较 to_string 和 to_value,src/bin/map_keys.rs:
use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
enum Color {
Red,
}
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
struct Id(u32);
#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
struct Point {
x: u32,
}
fn one<K: Serialize>(label: &str, key: K) {
// A one-entry map with no real map type behind it, so f64 keys need no Ord.
struct M<K>(K);
impl<K: Serialize> Serialize for M<K> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry(&self.0, &0)?;
m.end()
}
}
let text = serde_json::to_string(&M(&key));
let value = serde_json::to_value(M(&key)).map(|v| v.to_string());
println!("{label:<16} to_string={text:?}");
println!("{:<16} to_value ={value:?}", "");
}
fn main() {
one("&str", "a");
one("char", 'c');
one("u8", 7u8);
one("i128", -1i128);
one("bool", true);
one("f64", 1.5f64);
one("f64 NaN", f64::NAN);
one("unit variant", Color::Red);
one("newtype struct", Id(3));
one("Some(1)", Some(1));
one("None", None::<u32>);
one("()", ());
one("(u32, u32)", (1u32, 2u32));
one("Vec<u8>", vec![1u8]);
one("struct", Point { x: 1 });
let mut m = BTreeMap::new();
m.insert((1u32, 2u32), "a");
println!("BTreeMap<(u32,u32),_>: {:?}", serde_json::to_string(&m));
}
&str to_string=Ok("{\"a\":0}")
to_value =Ok("{\"a\":0}")
char to_string=Ok("{\"c\":0}")
to_value =Ok("{\"c\":0}")
u8 to_string=Ok("{\"7\":0}")
to_value =Ok("{\"7\":0}")
i128 to_string=Ok("{\"-1\":0}")
to_value =Ok("{\"-1\":0}")
bool to_string=Ok("{\"true\":0}")
to_value =Ok("{\"true\":0}")
f64 to_string=Ok("{\"1.5\":0}")
to_value =Ok("{\"1.5\":0}")
f64 NaN to_string=Err(Error("float key must be finite (got NaN or +/-inf)", line: 0, column: 0))
to_value =Err(Error("float key must be finite (got NaN or +/-inf)", line: 0, column: 0))
unit variant to_string=Ok("{\"Red\":0}")
to_value =Ok("{\"Red\":0}")
newtype struct to_string=Ok("{\"3\":0}")
to_value =Ok("{\"3\":0}")
Some(1) to_string=Ok("{\"1\":0}")
to_value =Err(Error("key must be a string", line: 0, column: 0))
None to_string=Err(Error("key must be a string", line: 0, column: 0))
to_value =Err(Error("key must be a string", line: 0, column: 0))
() to_string=Err(Error("key must be a string", line: 0, column: 0))
to_value =Err(Error("key must be a string", line: 0, column: 0))
(u32, u32) to_string=Err(Error("key must be a string", line: 0, column: 0))
to_value =Err(Error("key must be a string", line: 0, column: 0))
Vec<u8> to_string=Err(Error("key must be a string", line: 0, column: 0))
to_value =Err(Error("key must be a string", line: 0, column: 0))
struct to_string=Err(Error("key must be a string", line: 0, column: 0))
to_value =Err(Error("key must be a string", line: 0, column: 0))
BTreeMap<(u32,u32),_>: Err(Error("key must be a string", line: 0, column: 0))
- 能写成字符串的标量都可以当键,数字和布尔加上引号:
HashMap<u32, _>能序列化,HashMap<(u32, u32), _>不能,tuple 走的是serialize_tuple。struct 键同理,走serialize_struct。 - 错误的位置是
line: 0, column: 0:这些错误由Error::syntax(ErrorCode::KeyMustBeAString, 0, 0)构造,序列化一侧没有位置可报(为什么这样显示见 错误)。 - 唯一一处两边不一致的是
Some(1):写 JSON 文本的MapKeySerializer透过Some,to_value用的那个(value/ser.rs里同名的另一个MapKeySerializer)的serialize_some仍然是Err(key_must_be_a_string())。
浮点键和布尔键都是后来才放开的:f32 / f64 键来自 PR #1027(v1.0.101,同时加上了「Return error on non-finite float keys」),布尔键来自 PR #1055(v1.0.105,修 issue #1054「Cannot deserialize to HashMap<bool, _>」)。读回时这些带引号的键怎么还原成数字和布尔,在反序列化一侧。
Some 键在 to_string 里能用、在 to_value 里不能用,是一个只改了一半的 PRserde-rs/json PR #1030「Support MapKeySerializer::serialize_some」(首次发布于 v1.0.99)之前,ser.rs 的 MapKeySerializer::serialize_some 和其他不支持的方法一样返回 key_must_be_a_string。PR 的说明是「This PR updates MapKeySerializer::serialize_some to match Serializer::serialize_some.」理由是「Because there is no distinction between Some(T) and T in JSON, I think that serializing the underlying type directly is a better behavior for this function.」它也写明了反对意见:「Users may serialize a type like HashMap<Option<String>, T> with keys that are all Some(...), and then be surprised when the type fails to serialize with a key that is None. Currently, I think that any types that are valid map keys are always valid map keys, which is desirable.」
这个 PR 只改了 src/ser.rs(提交 b0fa978,3 行加、2 行删)。src/value/ser.rs 里给 to_value 用的 MapKeySerializer 没有跟着改,到 1.0.154 还是报错,上面实验里 Some(1) 那两行就是这么来的。PR 自己说的那个「新坑」也在实验里:None 键在两边都报错,所以 HashMap<Option<String>, T> 能不能序列化取决于运行时有没有 None 键。
to_writer、to_vec、to_string 与缓冲
三个入口一层套一层:to_string 调 to_vec,再用 from_utf8_unchecked 把 Vec<u8> 转成 String(不复制);to_vec 开一个 Vec::with_capacity(128),调 to_writer:
pub fn to_writer<W, T>(writer: W, value: &T) -> Result<()>
where
W: io::Write,
T: ?Sized + Serialize,
{
let mut ser = Serializer::new(writer);
value.serialize(&mut ser)
}
io::Write 在打开 std 时就是 std::io::Write(io/mod.rs 里 use std::io as imp;),没有 std 时是 serde_json 自己的一个精简版。to_writer 按值拿 W,&mut Vec<u8>、&mut File 也实现了 io::Write,所以传引用就能接着用原来的 writer。
serde_json 在 Serializer 和 writer 之间没有任何缓冲:前面每一个 Formatter 方法都是直接 writer.write_all(...)。写进 Vec 时这无所谓,write_all 就是一次 extend_from_slice;写进一个没有缓冲的 File 或 TcpStream 时,每次 write_all 至少是一次 write 系统调用。
Post 直接 to_writer 进一个数调用次数的 writer,write 被调用了几次?M2 Max、macOS、rustc 1.99.0。输出是 {"id":7,"title":"serde","tags":["rust","json"]},47 个字节。src/bin/quiz_writes.rs:
use serde::Serialize;
use std::io::{self, Write};
#[derive(Serialize)]
struct Post {
id: u32,
title: String,
tags: Vec<String>,
}
// Counts how many times the serializer calls write on us.
#[derive(Default)]
struct CountWrites {
calls: usize,
bytes: usize,
}
impl Write for CountWrites {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.calls += 1;
self.bytes += buf.len();
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn main() {
let post = Post { id: 7, title: "serde".into(), tags: vec!["rust".into(), "json".into()] };
let mut w = CountWrites::default();
serde_json::to_writer(&mut w, &post).unwrap();
println!("write calls={} bytes={}", w.calls, w.bytes);
}
如果目标是文件或套接字,自己包一层 BufWriter。同一个 Post 换成 BufWriter<CountWrites>,再看 to_writer_pretty,以及 to_vec 的分配次数(计数分配器同时数 alloc 和 realloc),src/bin/writes.rs 的 main:
fn main() {
let post = Post { id: 7, title: "serde".into(), tags: vec!["rust".into(), "json".into()] };
let mut buffered = BufWriter::new(CountWrites::default());
serde_json::to_writer(&mut buffered, &post).unwrap();
let inner = buffered.into_inner().map_err(|e| e.into_error()).unwrap();
println!("to_writer(BufWriter<CountWrites>) write calls={} bytes={}", inner.calls, inner.bytes);
let mut pretty = CountWrites::default();
serde_json::to_writer_pretty(&mut pretty, &post).unwrap();
println!("to_writer_pretty(CountWrites) write calls={} bytes={}", pretty.calls, pretty.bytes);
for n in [1usize, 10, 100, 1000] {
let posts: Vec<Post> = (0..n)
.map(|i| Post { id: i as u32, title: format!("post {i}"), tags: vec!["rust".into()] })
.collect();
let before = ALLOCS.load(Relaxed);
let v = serde_json::to_vec(&posts).unwrap();
let to_vec = ALLOCS.load(Relaxed) - before;
let before = ALLOCS.load(Relaxed);
let s = serde_json::to_string(&posts).unwrap();
let to_string = ALLOCS.load(Relaxed) - before;
let mut reuse = Vec::with_capacity(v.len());
let before = ALLOCS.load(Relaxed);
serde_json::to_writer(&mut reuse, &posts).unwrap();
let to_writer = ALLOCS.load(Relaxed) - before;
println!(
"{n:>4} posts: {:>6} bytes to_vec allocs={to_vec} to_string allocs={to_string} to_writer(presized Vec) allocs={to_writer}",
s.len()
);
}
}
(Post、CountWrites 和动脑筋里的一样,计数分配器和 Serialize 与 Serializer 里 collect_str 实验的那个相同,只多数了 realloc。)跑两次,输出相同:
to_writer(BufWriter<CountWrites>) write calls=1 bytes=47
to_writer_pretty(CountWrites) write calls=41 bytes=73
1 posts: 43 bytes to_vec allocs=1 to_string allocs=1 to_writer(presized Vec) allocs=0
10 posts: 421 bytes to_vec allocs=3 to_string allocs=3 to_writer(presized Vec) allocs=0
100 posts: 4381 bytes to_vec allocs=7 to_string allocs=7 to_writer(presized Vec) allocs=0
1000 posts: 45781 bytes to_vec allocs=10 to_string allocs=10 to_writer(presized Vec) allocs=0
BufWriter把所有小写入攒在它 8 KiB 的缓冲里,into_inner时一次写给下层。- 美化输出多了换行和缩进,每个都是单独的
write_all,次数更多。 to_vec的分配全是那个Vec的:一开始 128 字节,之后按Vec的倍增策略长到 256、512、1024……;43 字节一次就够,421 字节到 512 是 3 次,45781 字节到 65536 是 10 次。序列化本身(转义、itoa、zmij)一次也不分配。to_string和to_vec一样多,from_utf8_unchecked直接接管Vec的缓冲。- 事先按大小准备好的
Vec交给to_writer,0 次分配。反复序列化时,clear()之后复用同一个Vec就能省掉这些分配。
axum 的 Json 响应就是这么写的:into_response 里 BytesMut::with_capacity(128).writer() 再 serde_json::to_writer,注释说「Use a small initial capacity of 128 bytes like serde_json::to_vec」。BufMut::writer 怎么把 BytesMut 接到 io::Write 上,见 bytes 卷的 Reader 与 Writer;Json 怎么变成响应,见 axum 卷的 IntoResponse。
动脑筋答案
write calls=29 bytes=47
29 次。CountWrites::write 每次都把整个切片收下,write_all 调一次 write 就结束,所以 write 的次数就是 write_all 的次数。按输出逐个数:
- 6 个字符串(
id、title、serde、tags、rust、json),每个都是开引号、内容、闭引号三次,共 18 次;这些字符串都不含要转义的字节,内容只占一次write_string_fragment。 {、}、[、]4 次。- 3 个
:(begin_object_value)。 - 3 个
,:两个在title、tags两个键前(begin_object_key(first=false)),一个在json前(begin_array_value(first=false))。 71 次,itoa 格式化完一次写出。
18 + 4 + 3 + 3 + 1 = 29。47 个字节被拆成 29 次写入,平均每次不到 2 个字节;换成没有缓冲的 File,就是 29 次系统调用。
小结
Serializer<W, F>只有writer和formatter两个字段;实现serde::Serializer的是&mut Serializer,Ok = (),七个状态对象都是Compound。Compound::Map装着&mut Serializer和本层的State(Empty/First/Rest),只算出first: bool交给Formatter;每层一个Compound,Formatter被所有层共用,所以CompactFormatter没有字段。Formatter的方法全有默认实现,默认就是紧凑输出;PrettyFormatter用一个深度计数和一个has_value布尔就能写出缩进,空容器写成[]/{}。- 字符串转义查 256 项的
ESCAPE表,只转义 0x00–0x1F、"、\;不转义的连续字节一次写出;/、0x7F、U+2028 和所有非 ASCII 字符原样输出。 - 整数用 itoa、浮点用 zmij,都在栈上格式化;浮点写最短可读回的串,整数值保留
.0;NaN 和无穷作为值写成null,作为键报错。 MapKeySerializer把字符串、字符、整数、bool、有限浮点、unit 变体和 newtype 写成 JSON 字符串,其余报key must be a string;Some键只在写文本时被接受,to_value拒绝它。- serde_json 不缓冲:每个标点、每段字符串都是一次
write_all,一个 47 字节的Post是 29 次write;写文件或套接字要自己包BufWriter。to_vec从 128 字节开始按倍增长,复用一个Vec可以省掉这些分配。