Validation
Validation in rust-web-server is built around three pieces that compose cleanly:
Validate— a trait your types implement to declare field-level rules#[derive(Validate)]— a proc-macro that generates the implementation from annotationsValidated<T>— aFromRequestwrapper that extracts and validates in one step
The Validate trait
pub trait Validate { fn validate(&self) -> Result<(), ValidationErrors>;}Implement it manually when you need custom logic:
use rust_web_server::validate::{Validate, ValidationErrors};
struct Payload { name: String, score: u32,}
impl Validate for Payload { fn validate(&self) -> Result<(), ValidationErrors> { let mut errors = ValidationErrors::new();
if self.name.is_empty() { errors.add("name", "must not be empty"); } if self.score > 100 { errors.add("score", "must be 100 or less"); }
if errors.is_empty() { Ok(()) } else { Err(errors) } }}ValidationErrors
ValidationErrors accumulates every failing field so the caller sees all problems at once.
| Method | Description |
|---|---|
ValidationErrors::new() | Creates an empty collector |
.add(field, message) | Records one failure |
.is_empty() | Returns true when there are no errors |
.errors() | Returns &[FieldError] — each has .field and .message |
.into_json() | Serialises as {"errors":[{"field":"…","message":"…"}]} |
#[derive(Validate)]
Add features = ["macros"] to your dependency to unlock the derive macro.
[dependencies]rust-web-server = { version = "17", features = ["macros"] }Then annotate your struct fields with #[validate(...)] rules:
use rust_web_server::Validate;
#[derive(rust_web_server::Validate)]struct SignupForm { #[validate(length(min = 1, max = 50))] username: String,
#[validate(email)] email: String,
#[validate(length(min = 8, max = 128))] password: String,
#[validate(range(min = 13, max = 120))] age: u8,
#[validate(url)] website: String,
#[validate(required)] terms_accepted: String,}Supported validators
| Syntax | What it checks |
|---|---|
length(min = N) | field.chars().count() >= N |
length(max = N) | field.chars().count() <= N |
length(min = N, max = N) | both bounds |
range(min = N) | field as f64 >= N |
range(max = N) | field as f64 <= N |
range(min = N, max = N) | both bounds |
email | non-empty local part, exactly one @, domain contains . |
required | !field.is_empty() |
url | starts with http:// or https:// |
All failures are collected before returning — the caller always sees every invalid field.
Validated<T> extractor
Validated<T> implements FromRequest for any T: FromRequest + Validate. It first extracts T from the request, then runs validation:
- If extraction fails (e.g. invalid UTF-8) →
400 Bad Request - If validation fails →
422 Unprocessable Entitywith a JSON body
use rust_web_server::validate::{Validate, Validated, ValidationErrors};use rust_web_server::extract::{FromRequest, BodyText};use rust_web_server::request::Request;use rust_web_server::response::Response;
fn handle(req: &Request) -> Response { let Validated(form) = match Validated::<SignupForm>::from_request(req) { Ok(v) => v, Err(r) => return r, // 400 or 422 };
// form.username, form.email, etc. are all valid here Response::new()}The 422 body looks like:
{ "errors": [ { "field": "email", "message": "email must be a valid email address" }, { "field": "username", "message": "username must be at least 1 character(s) long" } ]}Complete example — signup endpoint
use rust_web_server::Validate;use rust_web_server::validate::{Validate as _, Validated, ValidationErrors};use rust_web_server::json::Json;use rust_web_server::extract::FromRequest;use rust_web_server::request::Request;use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};use rust_web_server::router::PathParams;use rust_web_server::server::ConnectionInfo;use rust_web_server::routes;use rust_web_server::app::App;use serde::Deserialize;
#[derive(Deserialize, rust_web_server::Validate)]struct SignupRequest { #[validate(length(min = 1, max = 50))] username: String,
#[validate(email)] email: String,
#[validate(length(min = 8))] password: String,}
impl FromRequest for SignupRequest { fn from_request(req: &Request) -> Result<Self, Response> { let Json(payload) = Json::<SignupRequest>::from_request(req)?; Ok(payload) }}
fn signup( req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _state: &(),) -> Response { let Validated(body) = match Validated::<SignupRequest>::from_request(req) { Ok(v) => v, Err(r) => return r, };
// At this point body.username, body.email, and body.password are valid. let mut r = Response::new(); r.status_code = *STATUS_CODE_REASON_PHRASE.n201_created.status_code; r.reason_phrase = STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string(); r}
let app = routes! { App::with_state(()), POST "/signup" => signup,};