Eserde:不止步于第一个反序列化错误
Eserde 是一个专门解决 Rust 中反序列化错误报告问题的库。与传统的 serde 不同,它能够一次性报告多个错误,而不是在遇到第一个错误时就终止。这极大地改善了开发者体验,减少了调试和修正的时间。
什么是问题?
Rust 的 serde 库是目前最流行的(反)序列化工具。但它的设计有一个缺陷:当发生反序列化错误时,serde 会立即停止并只返回第一个错误信息。这对用户提交的数据处理(如 REST API 请求体)来说是一个大问题。例如,如果用户提交了一个包含多处错误的 JSON 数据,API 只能一次反馈一个错误,迫使用户进入慢且令人沮丧的反馈循环:
- 发送请求。
- 收到一个错误。
- 修复错误。
- 回到第 1 步,直到所有错误都被修复。
这种方式对开发者来说并不友好。我们应该可以同时报告多个错误,从而减少 API 交互次数,提高效率。
案例分析:无效的 JSON 数据
考虑以下结构作为参考示例:
#[derive(Debug, serde::Deserialize)]
struct Package {
version: Version,
source: String,
}
#[derive(Debug, eserde::Deserialize)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
我们尝试通过 serde_json 对一个无效的 JSON 数据进行反序列化:
{
"version": {
"major": 1,
"minor": "2"
},
"source": null
}
代码如下:
let payload = r#"
{
"version": {
"major": 1,
"minor": "2"
},
"source": null
}"#;
let error = serde_json::from_str::<Package>(&payload).unwrap_err();
assert_eq!(
error.to_string(),
r#"invalid type: string "2", expected u32 at line 5 column 24"#
);
正如预期,serde_json 只返回了第一个错误:“字段 version.minor 类型错误”。
但是,这段 JSON 实际上还有其他问题:
version结构缺少patch字段。source字段不能为null。
切换到 eserde 后,我们可以一次性捕获所有这些错误:
#[derive(Debug, eserde::Deserialize)]
struct Package {
version: Version,
source: String,
}
#[derive(Debug, eserde::Deserialize)]
struct Version {
major: u32,
minor: u32,
patch: u32,
}
let payload = r#"
{
"version": {
"major": 1,
"minor": "2"
},
"source": null
}"#;
let errors = eserde::json::from_str::<Package>(&payload).unwrap_err();
assert_eq!(
errors.to_string(),
r#"Something went wrong during deserialization:
- version.minor: invalid type: string "2", expected u32 at line 5 column 24
- version: missing field `patch`
- source: invalid type: null, expected a string at line 7 column 22
"#
);
现在,我们可以在一次反馈中告诉用户需要修复的三个问题。
如何使用 eserde?
要在项目中使用 eserde,只需将以下依赖项添加到你的 Cargo.toml 文件中:
[dependencies]
eserde = { version = "0.1" }
serde = "1"
接下来,你需要:
- 将所有的
#[derive(serde::Deserialize)]替换为#[derive(eserde::Deserialize)]。 - 切换到基于
eserde的反序列化函数。
JSON 支持
eserde 提供了对 JSON 的原生支持,启用方法是在 Cargo.toml 中激活 json 特性:
[dependencies]
eserde = { version = "0.1", features = ["json"] }
serde = "1"
如果你正在处理 JSON 数据:
- 替换
serde_json::from_str为eserde::json::from_str。 - 替换
serde_json::from_slice为eserde::json::from_slice。
注意,eserde::json 不支持从读取器反序列化(即没有 serde_json::from_reader 的等价功能)。
此外,eserde 还提供了与 axum 的集成模块 eserde_axum,可以用作 axum 内置 JSON 解析器的替代方案。
兼容性
eserde 设计上最大程度地与 serde 兼容。derive(eserde::Deserialize) 会同时实现 serde::Deserialize 和 eserde::EDeserialize,并遵循所有受支持的 serde 属性行为。
如果某些字段未实现 eserde::EDeserialize,可以使用 #[eserde(compat)] 属性回退到 serde 的默认逻辑:
#[derive(eserde::Deserialize)]
struct Point {
#[eserde(compat)]
x: Latitude,
// [...]
}
更多细节请参考 eserde 的派生宏文档。
内部工作原理
以 JSON 为例(其他格式也适用),eserde 首先尝试通过 serde_json 进行反序列化。如果成功,则直接返回解析结果。如果失败,则执行第二次遍历输入数据,使用 eserde::EDeserialize::deserialize_for_errors 收集所有错误。
以下是 eserde::json::from_str 的简化实现:
pub fn from_str<'a, T>(s: &'a str) -> Result<T, DeserializationErrors>
where
T: EDeserialize<'a>,
{
let mut de = serde_json::Deserializer::from_str(s);
match T::deserialize(&mut de) {
Ok(v) => return Ok(v),
Err(_) => (),
}
// 开始错误收集
let _guard = ErrorReporter::start_deserialization();
let mut de = serde_json::Deserializer::from_str(s);
let de = path::Deserializer::new(&mut de);
let errors = match T::deserialize_for_errors(de) {
Ok(_) => vec![],
Err(_) => ErrorReporter::take_errors(),
};
if errors.is_empty() {
Err(vec![DeserializationError {
path: None,
details: "未知错误".to_string(),
}])
} else {
Err(errors)
}
}
EDeserialize::deserialize_for_errors 会在线程局部缓冲区中累积错误,并由 ErrorReporter 初始化和提取。
局限性与缺点
eserde 是一个新库,可能还存在一些未被发现的问题或缺陷。在生产环境中使用前,请务必进行充分测试。如遇任何问题,请在 GitHub 仓库 上提交 issue。
除了潜在的缺陷外,还有一些因设计导致的限制:
- 输入数据需要被访问两次,因此无法直接从不可重复读取的流中反序列化。
- 两次访问输入会导致性能比单次
serde::Deserialize更慢。 #[derive(eserde::Deserialize)]生成的代码量是serde::Deserialize的两倍左右,可能会显著增加编译时间。
尽管如此,我们认为对于面向用户的场景,这种权衡是值得的。
未来计划
- 添加对 YAML 和 TOML 等更多数据格式的支持。
- 增强对
#[serde]属性的支持,降低迁移成本。 - 集成验证功能,类似于
garde和validator,并在反序列化过程中直接完成验证,无需额外调用.validate()。
