带验证的 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
响应。这确保了无论哪种失败,前端收到的错误格式都是一致的。