rust 85 lines · 3 tabs

Accumulating Field-Level Validation Errors in Rust Signup Forms

Shared by codesnips Jul 2026
3 tabs
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct SignupRequest {
    pub email: String,
    pub password: String,
    pub password_confirmation: String,
    pub age: i32,
}

#[derive(Debug, Serialize)]
pub struct FieldError {
    pub field: &'static str,
    pub message: String,
}

#[derive(Debug)]
pub struct NewUser {
    pub email: String,
    pub password: String,
    pub age: u8,
}
3 files · rust Explain with highlit

Validating a signup payload well means reporting every problem at once, not bailing on the first bad field. A form that rejects a password, then rejects the email on the next round-trip, frustrates users and wastes requests. This snippet models an error-accumulating validator in Rust, keeping the happy path type-safe while gathering all field errors into a single structured response.

The SignupRequest DTO tab defines the raw inbound shape with serde Deserialize, matching whatever JSON the client sends. It is deliberately "loose" — email, password, and age are plain owned values that may be malformed. Alongside it lives FieldError, a Serialize struct carrying a field name and a human-readable message, so the API can return a flat list the frontend can attach to individual inputs.

The Validator tab holds the core pattern. Rather than returning Result and short-circuiting on the first failure, SignupRequest::validate pushes into a Vec<FieldError> as it checks each rule, then decides at the end. The push_err helper keeps each check to one readable line. Individual predicates like valid_email and password strength are kept small and independent so they can run regardless of what earlier fields did. Only after all checks run does it branch: if errors.is_empty() it constructs a validated NewUser (a distinct type that proves the data is clean), otherwise it returns Err(errors) with the full set. This is the "parse, don't validate" idea — the successful branch yields a different, stronger type, so downstream code cannot accidentally use an unvalidated SignupRequest.

The trade-off is that accumulation requires owning a mutable errors vector and running checks eagerly, versus the terser ? operator that stops early. For user-facing forms that cost is worth it; for internal invariants, fail-fast is usually better.

The HTTP handler tab shows the wiring under axum: the extractor deserializes the body, validate runs, and the result maps to either 201 Created or 422 Unprocessable Entity with the JSON error array. Returning 422 with a machine-readable body lets the client render inline messages without guessing. A subtle pitfall the code avoids is leaking the raw password into logs or responses — only the validated NewUser moves forward, and FieldError messages never echo the submitted secret.


Related snips

Share this code

Here's the card — post it anywhere.

Accumulating Field-Level Validation Errors in Rust Signup Forms — share card
Link copied