在現有 Cargo.toml 補上 validator 相關 crate(版本可視專案調整):
validator = "0.20"
validator_derive = "0.20"
在 DTO(models.rs)上用 derive 與 attribute
把要驗證的 DTO 加上 #[derive(Validate)],並在欄位上使用屬性。以下附上要改寫的 DTO。
use validator_derive::Validate;
#[derive(Deserialize, Validate)]
pub struct CreateUser {
// 介於 3 到 50 字元之間
#[validate(length(min = 3, max = 50))]
pub username: String,
// 驗證是否符合 email 格式
#[validate(email)]
pub email: String,
// 密碼至少 8 字元
#[validate(length(min = 8))]
pub password: String,
}
// UpdateUser:Option 欄位在 Some 時套用驗證
#[derive(Deserialize, Validate)]
pub struct UpdateUser {
#[validate(length(min = 3, max = 50))]
pub username: Option<String>,
#[validate(email)]
pub email: Option<String>,
#[validate(length(min = 8))]
pub password: Option<String>,
}
// LoginRequest:也可加驗證
#[derive(Serialize, Deserialize, Clone, Validate)]
pub struct LoginRequest {
#[validate(length(min = 1))]
pub username_or_email: String,
#[validate(length(min = 8))]
pub password: String,
}
說明
在 handler 內呼叫 validate() 並處理錯誤
在 handler(例如 create_user、update_user、login)接收 Json(payload) 後,先呼叫 payload.validate()。以下示範如何把 validator 錯誤轉成可回傳的 JSON。
use validator::{Validate, ValidationErrors};
// 把 ValidationErrors 轉成 { "errors": { "field": ["msg1", "msg2"], ... } }
pub fn validation_errors_to_json(errs: &ValidationErrors) -> serde_json::Value {
use serde_json::Value;
let mut map = serde_json::Map::new();
for (field, errors) in errs.field_errors().iter() {
let messages: Vec<String> = errors.iter().map(|fe| {
// 優先使用 message,若無則使用 code(或其他 fallback)
if let Some(msg) = &fe.message {
msg.clone().to_string()
} else {
fe.code.to_string().into()
}
}).collect();
map.insert(field.to_string(), Value::Array(messages.into_iter().map(Value::String).collect()));
}
json!({ "errors": Value::Object(map) })
}
pub async fn create_user(
Extension(pool): Extension<PgPool>,
Extension(mut redis): Extension<MultiplexedConnection>,
Json(payload): Json<CreateUser>,
) -> Result<impl IntoResponse, AppError> {
// DTO 驗證(同步)
if let Err(e) = payload.validate() {
let body = validation_errors_to_json(&e);
return Err((StatusCode::BAD_REQUEST, body.to_string()));
}
// 驗證通過後,再進行密碼雜湊與 DB 操作
// ...
}
pub async fn login(
Extension(pool): Extension<PgPool>,
Json(payload): Json<LoginRequest>,
) -> Result<impl IntoResponse, AppError> {
if let Err(e) = payload.validate() {
let body = validation_errors_to_json(&e);
return Err((StatusCode::BAD_REQUEST, body.to_string()));
}
// 驗證通過 → 查 DB、驗證密碼
}
建議回傳 400 Bad Request,body 為結構化 JSON,例如:
{
"errors": {
"email": ["invalid email"],
"password": ["length must be at least 8"]
}
}
這樣前端可以根據欄位顯示 field-level error。也可以把第一個錯誤抽出來回傳更簡潔的訊息,但建議保留完整錯誤以利除錯。
符合格式,正常建立用戶
POST http://127.0.0.1:3000/users
{
"username": "user2",
"email": "user2@a.com",
"password": "password"
}
格式不符,回傳錯誤
POST http://127.0.0.1:3000/users
{
"username": "",
"email": "not-email",
"password": "1"
}
回傳400 與錯誤JSON
{
"errors": {
"email": [
"email"
],
"password": [
"length"
],
"username": [
"length"
]
}
}
因為格式不符,登入被拒的範例
POST http://127.0.0.1:3000/users/login
{
"username_or_email": "",
"password": "1"
}
回傳的JSON
{
"errors": {
"password": [
"length"
],
"username_or_email": [
"length"
]
}
}
自訂驗證函式範例(跨欄位或更複雜)
fn validate_password_strength(pw: &str) -> Result<(), validator::ValidationError> {
if pw.chars().any(|c| c.is_ascii_punctuation()) && pw.len() >= 8 {
Ok(())
} else {
Err(validator::ValidationError::new("weak_password"))
}
}
#[derive(Deserialize, Validate)]
pub struct CreateUser {
// ...
#[validate(length(min = 8))]
#[validate(custom = "validate_password_strength")]
pub password: String,
}