在現有 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,
}