带验证的 JSON 提取器 (`VJson<T>`)
带验证的 JSON 提取器 (VJson<T>)
在现代前后端分离的 Web 应用中,JSON 是最主要的数据交换格式。当后端接收到来自客户端的 JSON 请求体时,我们不仅需要将其反序列化为 Rust 结构体,更重要的是要 验证 这些数据的合法性。
与处理表单类似,传统的做法是在 Handler 函数内部手动调用验证逻辑。为了遵循 DRY (Don't Repeat Yourself) 原则并提升代码质量,我们创建了一个自定义的 Axum 提取器 (Extractor)——VJson<T> (Validated JSON)。
VJson<T> 将 JSON 反序列化 和 数据验证 两个步骤无缝地集成在了一起。
核心理念
如果你的 Handler 代码能够执行,那么 VJson 中的数据就一定是已经通过了所有验证规则的 有效数据。你无需再进行任何手动的 validate() 调用或错误处理。
如何使用 VJson<T>
使用 VJson 的流程与 VForm 几乎完全一致,非常简单。
第 1 步:定义你的数据传输对象 (DTO)
首先,创建一个结构体来表示你期望接收的 JSON 数据。这个结构体通常被称为数据传输对象 (DTO - Data Transfer Object)。关键在于要为它派生 Deserialize (由 serde 提供) 和 Validate (由 validator 提供) 两个 Trait。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)] // 关键!
pub struct UserLoginDTO {
// ... 字段定义
}第 2 步:为字段添加验证注解
使用 validator 库提供的宏属性,为结构体的字段添加具体的验证规则。
use serde::Deserialize;
use validator::Validate;
#[derive(Deserialize, Validate)]
pub struct UserLoginDTO {
#[validate(email(message = "请输入有效的邮箱地址"))]
pub email: String,
#[validate(length(min = 6, max = 20, message = "密码长度必须在 6 到 20 个字符之间"))]
pub password: String,
}validator 库支持非常丰富的验证规则,如 email, url, length, range 等。你可以通过 message 属性自定义验证失败时的错误提示信息。
第 3 步:在 Handler 中使用 VJson<T>
现在,你可以在 Handler 的函数签名中直接使用 VJson<YourDTO> 来代替 Axum 原生的 Json<T>。
✅ 推荐做法 (使用 VJson)
use crate::common::validatedjson::VJson; // 引入 VJson
// Handler 代码极其简洁和专注
pub async fn login(
// 只需这一行!
// 如果 JSON 格式错误或验证失败,请求会直接被拒绝并返回 400/422 错误,
// 根本不会执行这个函数的代码。
VJson(dto): VJson<UserLoginDTO>,
) -> Response {
// 在这里,你可以完全信任 dto 中的数据是合法的
// 直接进入核心业务逻辑
let result = auth_service::login(dto).await;
ApiResponse::from_result(result)
}❌ 传统做法 (使用 Json,对比)
如果没有 VJson,你的代码会是这样,充满了模板式的验证和错误处理逻辑:
use axum::Json;
pub async fn login_legacy(
Json(dto): Json<UserLoginDTO>,
) -> Response {
// 1. 手动调用 validate()
if let Err(e) = dto.validate() {
// 2. 手动处理验证错误,并返回 422 响应 (422 Unprocessable Entity 更适合验证失败)
return ApiResponse::unprocessable_entity(e.to_string());
}
// 3. 只有通过了验证,才能继续执行业务逻辑
let result = auth_service::login(dto).await;
ApiResponse::from_result(result)
}对比之下,VJson 让我们的 Handler 代码变得更加干净和优雅。
核心优势
- 代码简洁 (Clean Code): 将验证逻辑从 Handler 中剥离,使其只关注核心业务,极大地提升了代码的可读性。
- 职责分离 (Separation of Concerns): 数据验证的职责被明确地交给了 DTO 自身(通过注解),而不是业务处理函数。
- 安全可靠 (Secure by Default): 从根本上避免了开发者忘记调用验证函数的风险。只要使用了
VJson,验证就是强制的。 - 统一错误处理 (Unified Error Handling): 无论是 JSON 格式错误、
Content-Type头缺失还是数据验证失败,VJson都会自动返回一个格式统一的、带有清晰错误信息的ApiResponse响应,前端可以获得一致的错误体验。
实现原理:FromRequest 与 ServerError
VJson 的“魔法”源于它为自己实现了 Axum 的 FromRequest Trait。当 Axum 看到 Handler 参数中有 VJson<T> 时,它会调用我们的自定义实现,这个实现会按顺序执行以下操作:
- 内部调用
Json<T>: 它首先使用 Axum 内置的Json<T>提取器来尝试解析请求体。如果失败(例如,JSON 格式无效、Content-Type头不是application/json),Json<T>会返回一个JsonRejection或MissingJsonContentType错误。 - 执行
.validate(): 如果 JSON 成功反序列化,它会接着调用value.validate()?。如果数据不符合你在 DTO 上定义的验证规则,.validate()会返回一个ValidationErrors错误。 - 返回成功或失败:
- 如果以上两步都成功,它会将解析并验证过的数据包装在
VJson中返回给 Handler。 - 如果任何一步失败,它会捕获错误,并将它们统一包装成我们自定义的
ServerError类型返回。
- 如果以上两步都成功,它会将解析并验证过的数据包装在
ServerError 枚举也实现了 IntoResponse Trait,它会自动将这几种错误转换为一个带有详细错误信息的、HTTP 状态码通常为 400 Bad Request 或 422 Unprocessable Entity 的 ApiResponse 响应。这确保了无论哪种失败,前端收到的错误格式都是一致的。