[博客翻译]eserde——一个永不停歇的serde


原文地址:https://github.com/mainmatter/eserde/tree/155af5fb3df9d998e6b6bb70aecb7ca49e9f07d5/eserde


Eserde:不止步于第一个反序列化错误

Eserde 是一个专门解决 Rust 中反序列化错误报告问题的库。与传统的 serde 不同,它能够一次性报告多个错误,而不是在遇到第一个错误时就终止。这极大地改善了开发者体验,减少了调试和修正的时间。


什么是问题?

Rust 的 serde 库是目前最流行的(反)序列化工具。但它的设计有一个缺陷:当发生反序列化错误时,serde 会立即停止并只返回第一个错误信息。这对用户提交的数据处理(如 REST API 请求体)来说是一个大问题。例如,如果用户提交了一个包含多处错误的 JSON 数据,API 只能一次反馈一个错误,迫使用户进入慢且令人沮丧的反馈循环:

  1. 发送请求。
  2. 收到一个错误。
  3. 修复错误。
  4. 回到第 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_streserde::json::from_str
  • 替换 serde_json::from_sliceeserde::json::from_slice

注意,eserde::json 不支持从读取器反序列化(即没有 serde_json::from_reader 的等价功能)。

此外,eserde 还提供了与 axum 的集成模块 eserde_axum,可以用作 axum 内置 JSON 解析器的替代方案。


兼容性

eserde 设计上最大程度地与 serde 兼容。derive(eserde::Deserialize) 会同时实现 serde::Deserializeeserde::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] 属性的支持,降低迁移成本。
  • 集成验证功能,类似于 gardevalidator,并在反序列化过程中直接完成验证,无需额外调用 .validate()