错误
主线位置:两条主线的失败出口。序列化主线上,
Serialize实现和Serializer都只能用S::Error::custom造错误;反序列化主线上,Visitor的默认方法、derive 生成的visit_map/visit_seq、__FieldVisitor用de::Error的七个构造函数报错,再由 serde_json 补上位置。Visitor默认方法为什么都报invalid_type,在 Deserialize、Deserializer 与 Visitor。
flowchart TB visitor["Visitor 默认方法"] --> it[invalid_type] derive["derive 的 visit_map"] --> mf["missing_field 等"] it --> fmtargs["format_args 拼句子"] mf --> fmtargs fmtargs --> custom["custom(msg)"] custom --> json[make_error] json --> line0["line = 0"] line0 --> fix[fix_position] fix --> out["加 at line 后缀"]
本页实验都在 M2 Max、macOS、rustc 1.99.0 上跑,依赖 serde = "=1.0.229"(derive)、serde_json = "=1.0.154";比较格式时另用 serde_yaml 0.9.34、toml 0.8.23、serde_urlencoded 0.7.1、bincode 1.3.3。
错误类型由格式定义
Serializer 和 Deserializer 都有一个关联类型 type Error,由格式自己定义。serde 只规定这个类型要能怎样被构造,不规定它长什么样、存什么。两个 trait 都由一个宏生成,ser::Error 只有一个方法:
macro_rules! declare_error_trait {
(Error: Sized $(+ $($supertrait:ident)::+)*) => {
// ...
pub trait Error: Sized $(+ $($supertrait)::+)* {
// ...
fn custom<T>(msg: T) -> Self
where
T: Display;
}
}
}
#[cfg(feature = "std")]
declare_error_trait!(Error: Sized + StdError);
#[cfg(not(feature = "std"))]
declare_error_trait!(Error: Sized + Debug + Display);
用宏的原因就在最后四行:开了 std 时,Error 要求 std::error::Error 作为父 trait;没开时只要求 Debug + Display。trait 的方法列表两边完全一样,只有父 trait 不同,Rust 没有「按 cfg 选父 trait」的语法,于是整个 trait 定义写两遍,用宏去掉重复。de::Error 用的是同样的写法。
StdError 这个名字在 ser 和 de 里都重新导出了,按三种情况指向不同的东西:
#[cfg(all(not(feature = "std"), no_core_error))]
#[doc(no_inline)]
pub use crate::std_error::Error as StdError;
#[cfg(not(any(feature = "std", no_core_error)))]
#[doc(no_inline)]
pub use core::error::Error as StdError;
#[cfg(feature = "std")]
#[doc(no_inline)]
pub use std::error::Error as StdError;
no_core_error 由 serde_core/build.rs 在编译器低于 1.81 时设置(「The Error trait became available in core in 1.81.」)。那时 no_std 下没有标准的 Error trait 可用,std_error.rs 就定义了一个形状相同的替身:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
}
它的文档说明了用途:支持 no_std 的格式可以无条件写 impl serde::ser::StdError for MySerError {},开了 std 时这一行实现的就是真正的 std::error::Error,没开时实现的是 core 的版本或这个替身,格式 crate 不用自己再写一层 cfg。serde_json 就是这么写的:impl serde::de::StdError for Error。
serde 自己唯一的具体错误类型是 de::value::Error,给 de::value 里那些小 Deserializer 用(见 value 模块与 IntoDeserializer)。有 alloc 时它就是一个 Box<str>;连 alloc 都没有时是 (),Display 只能输出固定的 Serde deserialization error。
同一个类型错误,交给不同格式,外层完全不同。src/bin/err_formats.rs 让每个格式把 true 读进 struct Page { n: u32 }:
#[derive(Deserialize, Debug)]
struct Page {
n: u32,
}
fn main() {
let e = serde_json::from_str::<Page>("{\"n\": true}").unwrap_err();
println!("serde_json: {e}");
let e = serde_yaml::from_str::<Page>("n: true").unwrap_err();
println!("serde_yaml: {e}");
let e = toml::from_str::<Page>("n = true").unwrap_err();
println!("toml: {}", e.to_string().trim_end().replace('\n', "\n "));
let e = serde_urlencoded::from_str::<Page>("n=true").unwrap_err();
println!("serde_urlencoded: {e}");
let e = bincode::deserialize::<Page>(&[1]).unwrap_err();
println!("bincode: {e}");
}
serde_json: invalid type: boolean `true`, expected u32 at line 1 column 10
serde_yaml: n: invalid type: boolean `true`, expected u32 at line 1 column 4
toml: TOML parse error at line 1, column 5
|
1 | n = true
| ^^^^
invalid type: boolean `true`, expected u32
serde_urlencoded: invalid digit found in string
bincode: io error: unexpected end of file
- 前三个格式中间那句
invalid type: booleantrue, expected u32一字不差,它来自 serde 的invalid_type默认实现(下一节);位置信息是各格式自己加的:serde_json 加后缀,serde_yaml 加字段路径前缀和后缀,toml 画出源码片段。 - serde_urlencoded 的值都是字符串,它对
u32调的是str::parse,报的是ParseIntError的原文,根本没走invalid_type。 - bincode 不自描述,
[1]只有一个字节,读u32要四个字节,报的是读到结尾。它不知道这里「本来」是什么类型。
问: 为什么 serde 不干脆定义一个 serde::Error,所有格式共用?
答: 因为错误里最有用的那部分只有格式知道。serde_json 要存行号列号,还要区分「语法错」和「I/O 错」(Category);toml 要存一段源码区间来画图;bincode 的错误多半是 I/O 错误或长度溢出,没有行号这回事。一个公共类型要么存不下这些,要么带上每个格式都用不到的字段。serde 只需要一件事:在 Visitor 和 derive 代码里,不知道具体格式的情况下也能造出一个错误,所以它只规定构造函数,具体类型留给 D::Error。代价是调用者拿到的是 serde_json::Error 而不是一个统一类型,要跨格式处理只能靠 Display 或 std::error::Error。
de::Error 的七个构造函数
de::Error 除了必须实现的 custom,还有七个带默认实现的构造函数,全部标了 #[cold],全部只是用 format_args! 拼一句话再交给 custom:
pub trait Error: Sized $(+ $($supertrait)::+)* {
fn custom<T>(msg: T) -> Self
where
T: Display;
#[cold]
fn invalid_type(unexp: Unexpected, exp: &dyn Expected) -> Self {
Error::custom(format_args!("invalid type: {}, expected {}", unexp, exp))
}
#[cold]
fn invalid_value(unexp: Unexpected, exp: &dyn Expected) -> Self {
Error::custom(format_args!("invalid value: {}, expected {}", unexp, exp))
}
#[cold]
fn invalid_length(len: usize, exp: &dyn Expected) -> Self {
Error::custom(format_args!("invalid length {}, expected {}", len, exp))
}
#[cold]
fn unknown_variant(variant: &str, expected: &'static [&'static str]) -> Self {
if expected.is_empty() {
Error::custom(format_args!(
"unknown variant `{}`, there are no variants",
variant
))
} else {
Error::custom(format_args!(
"unknown variant `{}`, expected {}",
variant,
OneOf { names: expected }
))
}
}
#[cold]
fn unknown_field(field: &str, expected: &'static [&'static str]) -> Self {
// ... 同上,"unknown field `{}`, there are no fields" / "unknown field `{}`, expected {}"
}
#[cold]
fn missing_field(field: &'static str) -> Self {
Error::custom(format_args!("missing field `{}`", field))
}
#[cold]
fn duplicate_field(field: &'static str) -> Self {
Error::custom(format_args!("duplicate field `{}`", field))
}
}
几点设计:
- 默认实现都落到
custom。 一个格式只实现custom就能得到全部七种报错,措辞在所有格式里一致。想改措辞的格式可以覆盖个别方法(serde_json 覆盖了invalid_type和invalid_value,见后文)。 format_args!而不是format!。custom收的是T: Display,format_args!不分配,格式自己决定要不要把它变成String(serde_json 的custom里才to_string()),no_std 的格式可以完全不分配。#[cold]。 告诉编译器这些函数很少被调用,调用它们的分支按冷路径处理。- 字段名和变体名是
&'static str。 它们来自 derive 生成的FIELDS/VARIANTS常量,missing_field和duplicate_field的参数也是'static,因为名字总是编译期已知的;而unknown_*的第一个参数是输入里读到的名字,只能是普通的&str。
OneOf 按名字个数选三种句式:
impl Display for OneOf {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self.names.len() {
0 => panic!(), // special case elsewhere
1 => write!(formatter, "`{}`", self.names[0]),
2 => write!(formatter, "`{}` or `{}`", self.names[0], self.names[1]),
_ => {
tri!(formatter.write_str("one of "));
for (i, alt) in self.names.iter().enumerate() {
if i > 0 {
tri!(formatter.write_str(", "));
}
tri!(write!(formatter, "`{}`", alt));
}
Ok(())
}
}
}
}
0 => panic!() 不会被触发,因为两个调用者都先判断了 expected.is_empty(),走 there are no ... 的句式。
derive 生成的代码分别在这些地方调它们(生成代码的全貌在 derive 生成的 Deserialize):
| 构造函数 | 谁调 | 什么时候 |
|---|---|---|
invalid_type | Visitor 的默认 visit_*;serde_json 的 peek_invalid_type | 输入的种类不对(要数字给了字符串) |
invalid_value | 标准库类型的 Visitor(整数范围检查、char) | 种类对,值不对 |
invalid_length | 生成的 visit_seq;元组、数组的 impl | 序列元素不够 |
unknown_variant | 生成的变体标识 Visitor | 变体名不在 VARIANTS 里 |
unknown_field | 生成的字段标识 Visitor,只在 deny_unknown_fields 时 | 字段名不在 FIELDS 里 |
missing_field | __private229::de::missing_field,或带 deserialize_with 的字段直接调 | map 读完了,字段还是 None |
duplicate_field | 生成的 visit_map | 同一个字段第二次出现 |
missing_field 绕了一步:生成代码调的是 serde/src/private/de.rs 的 missing_field,它构造一个 MissingFieldDeserializer,对字段类型调一次 Deserialize::deserialize。这个 Deserializer 的 deserialize_option 调 visit_none,其余全部 Err(Error::missing_field(self.0))。于是 Option<T> 字段缺了就是 None,别的类型才报错;判断「是不是 Option」交给了类型系统,不靠 derive 看字段的写法。
src/bin/err_ctors.rs 对每个构造函数造一个能触发它的输入(类型定义省略:Post { title: String, views: u32 }、#[serde(deny_unknown_fields)] Strict { title: String }、空的 Empty {} 也加了 deny_unknown_fields、enum Color { Red, Green }、enum Shape { Circle, Square, Triangle }、空的 enum Never {}):
fn show<T: for<'de> Deserialize<'de> + std::fmt::Debug>(ty: &str, input: &str) {
match serde_json::from_str::<T>(input) {
Ok(v) => println!("{ty:<10} {input:<32} Ok({v:?})"),
Err(e) => println!("{ty:<10} {input:<32} {e}"),
}
}
fn main() {
println!("-- invalid_type");
show::<u32>("u32", r#""1""#);
show::<u32>("u32", "1.5");
// ... 其余每行一个输入,见下面的输出
}
-- invalid_type
u32 "1" invalid type: string "1", expected u32 at line 1 column 3
u32 1.5 invalid type: floating point `1.5`, expected u32 at line 1 column 3
u32 null invalid type: null, expected u32 at line 1 column 4
u32 true invalid type: boolean `true`, expected u32 at line 1 column 4
u32 [1] invalid type: sequence, expected u32 at line 1 column 0
u32 {} invalid type: map, expected u32 at line 1 column 0
Post -3 invalid type: integer `-3`, expected struct Post at line 1 column 2
-- invalid_value
u8 300 invalid value: integer `300`, expected u8 at line 1 column 3
u8 -1 invalid value: integer `-1`, expected u8 at line 1 column 2
char "ab" invalid value: string "ab", expected a character at line 1 column 4
-- invalid_length
(u8, u8) [1] invalid length 1, expected a tuple of size 2 at line 1 column 3
[u8; 3] [1, 2] invalid length 2, expected an array of length 3 at line 1 column 6
Post ["t"] invalid length 1, expected struct Post with 2 elements at line 1 column 5
-- unknown_variant
Color "Blue" unknown variant `Blue`, expected `Red` or `Green` at line 1 column 6
Shape "Hexagon" unknown variant `Hexagon`, expected one of `Circle`, `Square`, `Triangle` at line 1 column 9
Never "X" unknown variant `X`, there are no variants at line 1 column 3
-- unknown_field
Strict {"title":"t","tags":[]} unknown field `tags`, expected `title` at line 1 column 19
Empty {"x":1} unknown field `x`, there are no fields at line 1 column 4
-- missing_field
Post {"title":"t"} missing field `views` at line 1 column 13
-- duplicate_field
Post {"title":"a","title":"b"} duplicate field `title` at line 1 column 20
expected后面的半句来自Visitor::expecting:u32、struct Post、a tuple of size 2、struct Post with 2 elements都是各个Visitor自己写的。u8读到-1是invalid value而不是invalid type:负整数对u8来说种类是对的(整数),只是超出范围。整数之间的范围检查在 标准库类型的实现。struct Post从数组["t"]读:serde_json 的deserialize_struct也接受数组,按字段顺序读,元素不够就是invalid_length。null打印成null而不是 serde 默认的unit value,1.5有反引号:这是 serde_json 覆盖了invalid_type,下一节讲。
Unexpected:输入里实际是什么
Unexpected 描述「输入里遇到了什么」,大体上和 Visitor 的方法一一对应:Bool(bool)、Unsigned(u64)、Signed(i64)、Float(f64)、Char(char)、Str(&'a str)、Bytes(&'a [u8])、Unit、Option、NewtypeStruct、Seq、Map、Enum、UnitVariant、NewtypeVariant、TupleVariant、StructVariant,外加 Other(&'a str)。整数只有 u64 / i64 两档,所以 u128 的值放不进来。它带一个生命周期 'a,Str 和 Bytes 借用 visit_str 的参数,造错误时不必先拷贝一份。
Display 把它们写成名词短语,正好填进 invalid type: {}:
impl<'a> fmt::Display for Unexpected<'a> {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
use self::Unexpected::*;
match *self {
Bool(b) => write!(formatter, "boolean `{}`", b),
Unsigned(i) => write!(formatter, "integer `{}`", i),
Signed(i) => write!(formatter, "integer `{}`", i),
Float(f) => write!(formatter, "floating point `{}`", WithDecimalPoint(f)),
Char(c) => write!(formatter, "character `{}`", c),
Str(s) => write!(formatter, "string {:?}", s),
Bytes(_) => formatter.write_str("byte array"),
Unit => formatter.write_str("unit value"),
Option => formatter.write_str("Option value"),
NewtypeStruct => formatter.write_str("newtype struct"),
Seq => formatter.write_str("sequence"),
Map => formatter.write_str("map"),
Enum => formatter.write_str("enum"),
UnitVariant => formatter.write_str("unit variant"),
NewtypeVariant => formatter.write_str("newtype variant"),
TupleVariant => formatter.write_str("tuple variant"),
StructVariant => formatter.write_str("struct variant"),
Other(other) => formatter.write_str(other),
}
}
}
Str 用 {:?},引号和转义由 Debug 加;Bytes 不打印内容,只说 byte array。Float 包了一层 WithDecimalPoint:它用一个包装的 fmt::Write 记下 {} 输出里有没有出现过 .,有限值没有小数点就补一个 .0,NaN 和无穷大原样输出。
serde_json 没有直接用这个 Display。它的 invalid_type 和 invalid_value 套了一层 JsonUnexpected:
impl<'a> Display for JsonUnexpected<'a> {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
de::Unexpected::Unit => formatter.write_str("null"),
de::Unexpected::Float(value) => write!(
formatter,
"floating point `{}`",
zmij::Buffer::new().format(value),
),
unexp => Display::fmt(&unexp, formatter),
}
}
}
Unit 换成 JSON 用户认得的 null;Float 改用 zmij(serde_json 序列化浮点数用的同一个库)格式化。src/bin/err_unexpected.rs 把每个变体分别交给 serde::de::value::Error::invalid_type 和 serde_json::Error::invalid_type,截出中间那段比较:
use serde::de::{Error as _, Unexpected};
type Plain = serde::de::value::Error;
type Json = serde_json::Error;
fn main() {
let cases = [
Unexpected::Bool(false),
// ... 每个变体一个,Float 给了 1.5、2.0、1e20、NaN
Unexpected::Other("unoriginal superhero"),
];
println!("{:<34} {:<34} {}", "variant (Debug)", "serde Display", "serde_json");
for u in cases {
let j = Json::invalid_type(u, &"T").to_string();
let j = j.trim_start_matches("invalid type: ").trim_end_matches(", expected T");
println!("{:<34} {:<34} {}", format!("{u:?}"), u.to_string(), j);
}
}
variant (Debug) serde Display serde_json
Bool(false) boolean `false` boolean `false`
Unsigned(7) integer `7` integer `7`
Signed(-7) integer `-7` integer `-7`
Float(1.5) floating point `1.5` floating point `1.5`
Float(2.0) floating point `2.0` floating point `2.0`
Float(1e20) floating point `100000000000000000000.0` floating point `1e+20`
Float(NaN) floating point `NaN` floating point `NaN`
Char('x') character `x` character `x`
Str("a\"b\n") string "a\"b\n" string "a\"b\n"
Bytes([97, 98]) byte array byte array
Unit unit value null
Option Option value Option value
NewtypeStruct newtype struct newtype struct
Seq sequence sequence
Map map map
Enum enum enum
UnitVariant unit variant unit variant
NewtypeVariant newtype variant newtype variant
TupleVariant tuple variant tuple variant
StructVariant struct variant struct variant
Other("unoriginal superhero") unoriginal superhero unoriginal superhero
只有两行不同:Float(1e20) 在 serde 里是 21 位数字加 .0,在 serde_json 里是 1e+20;Unit 是 unit value 对 null。Other 的文档要求消息是「a noun or noun phrase, not capitalized and without a period」,示例就是 unoriginal superhero,因为它要原样填进句子中间。
31.0.196 之前,Unexpected::Float 直接用 {} 打印 f64,3.0 输出 3。被关掉的 PR #2679(「Format invalid float message to show at least one decimal place」)描述了问题:报错「invalid type: floating point 3, expected u32」「shows a valid integer but it is actually a float」。实际合并的是 PR #2682(「Format Unexpected::Float with decimal point」),加了上面的 WithDecimalPoint,测试里的期望从 floating point `0` 改成了 floating point `0.0` ;1.0.196 的发布说明:「Improve formatting of "invalid type" error messages involving floats (#2682)」。
serde_json 在同一天合并了 serde-rs/json PR #1107(「Format f64 in error messages using ryu」,1.0.112),也就是 JsonUnexpected 的 Float 分支:「This produces a more readable representation using exponential notation for values that might be hundreds of digits when formatted by std::fmt」,同时「includes trailing .0 on integer values」。上面实验里 1e20 的两种输出,就是 WithDecimalPoint 和这个分支的区别。后来 serde-rs/json PR #1304(1.0.147,「Switch from ryu -> zmij for float formatting」)把 ryu 换成了 zmij,所以今天的代码里是 zmij::Buffer。
Expected:本来想要什么
Expected 只有一个和 Display::fmt 同签名的方法,serde 给了两个实现:
impl<'de, T> Expected for T
where
T: Visitor<'de>,
{
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
self.expecting(formatter)
}
}
impl Expected for &str {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(self)
}
}
impl Display for dyn Expected + '_ {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
Expected::fmt(self, formatter)
}
}
- 每个
Visitor自动是一个Expected,所以在Visitor的方法里报错时写&self就够了,expecting写的那句话会出现在expected后面。 - 在
Visitor之外(比如手写的deserialize_with函数里),用一个字符串字面量&"a port number"。 - 构造函数收的是
&dyn Expected,format_args!需要Display,最后那个 impl 把两者接起来。
文档对 expecting 的要求和 Other 一样:补完「This Visitor expects to receive ...」这句话,不大写、不加句号。src/bin/err_expected.rs 写一个只收偶数的 Visitor:
struct EvenVisitor;
impl<'de> Visitor<'de> for EvenVisitor {
type Value = Even;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an even number")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Even, E> {
if v % 2 == 0 {
Ok(Even(v))
} else {
// the visitor itself is the Expected
Err(E::invalid_value(Unexpected::Unsigned(v), &self))
}
}
}
impl<'de> de::Deserialize<'de> for Even {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Even, D::Error> {
d.deserialize_u64(EvenVisitor)
}
}
fn main() {
for input in ["4", "3", "-2", "\"4\""] {
match serde_json::from_str::<Even>(input) {
Ok(v) => println!("{input:<5} Ok({v:?})"),
Err(e) => println!("{input:<5} {e}"),
}
}
let e: serde_json::Error = de::Error::invalid_value(Unexpected::Signed(-1), &"a port number");
println!("&str {e}");
}
4 Ok(Even(4))
3 invalid value: integer `3`, expected an even number at line 1 column 1
-2 invalid type: integer `-2`, expected an even number at line 1 column 2
"4" invalid type: string "4", expected an even number at line 1 column 3
&str invalid value: integer `-1`, expected a port number
3 是自己写的 invalid_value;-2 和 "4" 没有对应的方法,落到 Visitor 默认的 visit_i64 / visit_str,报 invalid_type,但 expected 后面同样是 an even number。直接构造的那一个没有位置:它没经过 Deserializer。
serde_json 怎么附上行列号
serde_json::Error 是一个 Box<ErrorImpl>,字段上的注释给了理由:「This Box allows us to keep the size of Error as small as possible. A larger Error type was substantially slower due to all the functions that pass around Result<T, Error>.」ErrorImpl 三个字段:
struct ErrorImpl {
code: ErrorCode,
line: usize,
column: usize,
}
ErrorCode 是 serde_json 自己的错误码(EofWhileParsingList、TrailingCharacters、KeyMustBeAString……),所有经 serde 构造函数来的错误都是其中的 Message(Box<str>)。Display 决定后缀:
impl Display for ErrorImpl {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.line == 0 {
Display::fmt(&self.code, f)
} else {
write!(
f,
"{} at line {} column {}",
self.code, self.line, self.column
)
}
}
}
行号从 1 开始,所以 line == 0 就表示「没有位置」。Visitor 里造的错误一开始都是 0:custom 调 make_error,那时还不知道位置。位置是 Deserializer 在调完 Visitor 之后补的,fix_position:
pub(crate) fn fix_position<F>(self, f: F) -> Self
where
F: FnOnce(ErrorCode) -> Error,
{
if self.err.line == 0 {
f(self.err.code)
} else {
self
}
}
Deserializer 一侧的 fix_position 传进去的闭包是 self.error(code),用 self.read.position() 重新造一个带位置的错误。前面 deserialize_str 的代码最后就是 Err(err) => Err(self.fix_position(err))。已经有位置的错误原样返回,所以一个错误从嵌套很深的 Visitor 冒出来,只在最里层那个 deserialize_* 被填一次,不会被外层改写。
position() 给的是「已经读过的位置」:列号是当前行里已经消费的字节数。所以 "1" 读成 u32 报 column 3(整个字符串读完才知道它是字符串),missing field 指向 } 之后,而 [1] 读成 u32 报 column 0:peek_invalid_type 看到左方括号就报错,一个字节都没消费。
位置还有一个反方向的入口。make_error 先尝试从消息末尾解析出位置:
// Parse our own error message that looks like "{} at line {} column {}" to work
// around erased-serde round-tripping the error through de::Error::custom.
fn make_error(mut msg: String) -> Error {
let (line, column) = parse_line_col(&mut msg).unwrap_or((0, 0));
Error {
err: Box::new(ErrorImpl {
code: ErrorCode::Message(msg.into_boxed_str()),
line,
column,
}),
}
}
注释说明了原因:erased-serde 会让错误经 de::Error::custom 转一圈,传进 custom 的是已经 Display 过、带着位置后缀的字符串;不解析回来,位置就只剩消息里的文字,line() 返回 0。这段代码从 1.0.37 起就有。parse_line_col 用 rfind(" at line ") 找后缀,要求后面紧跟数字、column、数字,并且一直到字符串结尾;成功就把后缀截掉,把数字放回 line / column。
serde_json::Error::custom("boom at line 7 column 9") 的 line() 是几?M2 Max、macOS、rustc 1.99.0。和 custom("boom") 对比:两个错误的 line()、column()、classify() 和 Display 各是什么?代码在下一节的 src/bin/err_classify.rs 里(最后两行)。
serde_json 的 Debug 也是自己写的,注释说「Humans often end up seeing this representation because it is what unwrap() shows」,所以它去掉了 Box 和 ErrorImpl 两层,直接打印消息和位置(src/bin/err_debug.rs):
Display: invalid type: string "1", expected u32 at line 1 column 3
Debug: Error("invalid type: string \"1\", expected u32", line: 1, column: 3)
Display: invalid type: sequence, expected u32 at line 1 column 0
classify:把错误分成四类
Error::classify 把 ErrorCode 映射到 Category 的四个值:
pub fn classify(&self) -> Category {
match self.err.code {
ErrorCode::Message(_) => Category::Data,
ErrorCode::Io(_) => Category::Io,
ErrorCode::EofWhileParsingList
| ErrorCode::EofWhileParsingObject
| ErrorCode::EofWhileParsingString
| ErrorCode::EofWhileParsingValue => Category::Eof,
ErrorCode::ExpectedColon
// ... 其余 17 个语法错误码
| ErrorCode::RecursionLimitExceeded => Category::Syntax,
}
}
is_io / is_syntax / is_data / is_eof 就是 classify() == Category::X。规则很简单:所有经 custom 来的都是 Data,也就是 serde 的七个构造函数、Visitor 和 Serialize 实现自己造的错误;serde_json 解析器自己发现的问题按错误码分到 Syntax 或 Eof;读 io::Read 失败是 Io。From<Error> for io::Error 也按这个分:Io 取出原来的 io::Error,Eof 变 UnexpectedEof,其余变 InvalidData。
src/bin/err_classify.rs:
use std::collections::HashMap;
use std::io::{self, Read};
use serde::de::Error as _;
use serde::{Serialize, Serializer};
struct Broken;
impl Read for Broken {
fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
Err(io::Error::other("disk on fire"))
}
}
struct Refuse;
impl Serialize for Refuse {
fn serialize<S: Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
Err(serde::ser::Error::custom("refusing to serialize"))
}
}
fn report(label: &str, e: serde_json::Error) {
println!(
"{label:<14} {:<7} line={} col={} {e}",
format!("{:?}", e.classify()),
e.line(),
e.column()
);
}
fn main() {
report("syntax", serde_json::from_str::<serde_json::Value>("{\"a\":}").unwrap_err());
report("eof", serde_json::from_str::<Vec<u32>>("[1,\n 2").unwrap_err());
report("data", serde_json::from_str::<u32>("\n\n \"x\"").unwrap_err());
report("trailing", serde_json::from_str::<u32>("1 2").unwrap_err());
report("io", serde_json::from_reader::<_, u32>(Broken).unwrap_err());
report("ser custom", serde_json::to_string(&Refuse).unwrap_err());
let mut m = HashMap::new();
m.insert((1u32, 2u32), 3);
report("ser key", serde_json::to_string(&m).unwrap_err());
report("custom", serde_json::Error::custom("boom"));
report("custom+suffix", serde_json::Error::custom("boom at line 7 column 9"));
}
前七行的输出(最后两行是动脑筋的答案):
syntax Syntax line=1 col=6 expected value at line 1 column 6
eof Eof line=2 col=2 EOF while parsing a list at line 2 column 2
data Data line=3 col=5 invalid type: string "x", expected u32 at line 3 column 5
trailing Syntax line=1 col=3 trailing characters at line 1 column 3
io Io line=0 col=0 disk on fire
ser custom Data line=0 col=0 refusing to serialize
ser key Syntax line=0 col=0 key must be a string
- 序列化错误没有位置(
line=0),后缀也就不出现。serde_json 的ser::Error::custom和de::Error::custom走的是同一个make_error。 HashMap<(u32, u32), _>序列化失败被归为Syntax:KeyMustBeAString是 serde_json 自己的错误码,在classify里和解析错误列在一起,尽管这里没有任何输入被解析。Io错误由Error::io构造,line和column都是 0。
axum 的 Json 提取器就是靠这个分类区分状态码的:Data 回 422,Syntax 和 Eof 回 400(请求体提取器)。
动脑筋答案
src/bin/err_classify.rs 的最后两行:
custom Data line=0 col=0 boom
custom+suffix Data line=7 col=9 boom at line 7 column 9
custom("boom"):parse_line_col找不到at line,位置是(0, 0),Display不加后缀。custom("boom at line 7 column 9"):make_error把后缀当成 serde_json 自己的格式解析出来,line()是 7、column()是 9,消息被截成boom;Display时又按line != 0把后缀拼回去,所以打印出来和输入一模一样。两者都是Data,因为都是ErrorCode::Message。
这也意味着,一个 Visitor 如果用 custom 报一条恰好以 at line N column M 结尾的消息,serde_json 会把它当成位置,并且 fix_position 看到 line != 0,不会再填真正的位置。
小结
- 错误类型由格式定义。
ser::Error只要求custom;de::Error另有invalid_type、invalid_value、invalid_length、unknown_variant、unknown_field、missing_field、duplicate_field七个#[cold]默认实现,全都用format_args!拼句子交给custom。 - 两个 trait 用
declare_error_trait!生成两遍,只为在std下加StdError父 trait;StdError重新导出std/core的Error,编译器低于 1.81 的 no_std 构建用std_error.rs的替身。 invalid type: string "x", expected u32=Unexpected的Display+Expected(Visitor::expecting或&str)。Float经WithDecimalPoint补.0(1.0.196,#2682);serde_json 用JsonUnexpected把Unit写成null、浮点用 zmij 格式化。missing_field经MissingFieldDeserializer绕一步,让Option<T>字段缺失时得到None。- serde_json 的
Error是Box<ErrorImpl>;line == 0表示没有位置,Deserializer::fix_position在Visitor返回后用已读位置补上,只补一次;make_error会从消息末尾的at line L column C反解出位置(为 erased-serde)。 classify:custom来的都是Data,解析器的错误码分Syntax/Eof,读失败是Io;axum 据此回 422 或 400。