RUST Contents

Input Validation Basics

Validate untrusted input at the boundaries: parse into strong types, enforce size and format limits, and return consistent errors so production services stay safe and predictable.

On this page

Validation Is a Boundary Responsibility

Production services accept untrusted input: HTTP requests, JSON payloads, headers, query parameters, and environment variables. Input validation is the discipline of turning untrusted bytes into trusted, typed data before it reaches domain logic.

Production mindset:

  • Validate early (at the boundary)
  • Fail fast with clear errors
  • Enforce size limits to prevent abuse
  • Keep domain logic working with trusted types

What to Validate (Minimal but Realistic)

Even without extreme edge cases, production validation should cover:

  • Required fields present
  • Type parsing (numbers, UUIDs, enums)
  • Format constraints (email-like strings, identifiers)
  • Length limits (strings, arrays)
  • Value ranges (page size, timeouts)

Parse Into Strong Types (Newtype Pattern)

A production-friendly pattern is to validate once and carry a safe type.

#[derive(Debug, Clone)]
struct Email(String);

impl Email {
    fn parse(input: &str) -> Result<Self, String> {
        let s = input.trim();
        if s.len() > 254 {
            return Err("email too long".to_string());
        }
        if !s.contains("@") {
            return Err("invalid email format".to_string());
        }
        Ok(Self(s.to_string()))
    }
}

Once you have Email, you do not re-validate it in every function.

Validate Size Limits First

Size limits protect your service from memory pressure and denial-of-service patterns. Always check size before expensive parsing.

fn validate_name(input: &str) -> Result<String, String> {
    if input.len() > 128 {
        return Err("name too long".to_string());
    }
    Ok(input.trim().to_string())
}

Production rule: treat size limits as part of your security posture, not just data cleanliness.

Validate Numeric Ranges

Pagination and timeouts are common production foot-guns. A client can request an extreme limit and cause heavy load.

fn parse_page_limit(input: &str) -> Result<u32, String> {
    let n: u32 = input.parse().map_err(|_| "limit must be a number".to_string())?;
    if n == 0 || n > 100 {
        return Err("limit must be between 1 and 100".to_string());
    }
    Ok(n)
}

Production rule: enforce hard caps.

Enums and Allowed Values

When input is from a small allowed set, map it to an enum.

#[derive(Debug, Clone, Copy)]
enum SortOrder {
    Asc,
    Desc,
}

impl SortOrder {
    fn parse(s: &str) -> Result<Self, String> {
        match s {
            "asc" => Ok(Self::Asc),
            "desc" => Ok(Self::Desc),
            _ => Err("sort must be asc or desc".to_string()),
        }
    }
}

This prevents "magic strings" flowing into deeper layers.

Validate at the Edge, Not Everywhere

A common anti-pattern is sprinkling validation checks inside domain functions. Instead:

  • HTTP layer parses/validates request into a domain command struct
  • Domain layer assumes inputs are already valid and focuses on business rules

Example command type:

struct CreateUserCommand {
    email: Email,
    nickname: Option<String>,
}

Production benefit: fewer scattered checks, more predictable behavior.

Consistent Error Responses

Validation errors should be consistent so clients can handle them and logs stay clean.

Minimal structured error approach:

#[derive(Debug)]
enum ValidationError {
    MissingField(&'static str),
    InvalidField(&'static str),
    TooLong(&'static str),
}

Then map to HTTP 400 with a stable payload format at the boundary.

Do Not Trust Deserialization Alone

Serde will deserialize types, but it does not enforce your business constraints (length caps, ranges, allowed sets). Always validate after deserialization.

Production pattern:

  • Deserialize JSON into an input struct (raw types)
  • Validate and convert into domain-safe types

Observability: Log Validation Failures Carefully

Validation errors can indicate client bugs or abuse. Log them with stable fields, but avoid logging raw payloads.

tracing::warn!(field = "email", "validation failed");

Production rule: do not log full request bodies (PII and secrets risk).

Common Production Pitfalls

  • No size limits (DoS risk)
  • Validation scattered across layers
  • Trusting deserialization as validation
  • Returning inconsistent error formats
  • Logging sensitive payloads during validation failures

Production Checklist

  • Validate at boundaries (HTTP/env/CLI)
  • Enforce length and range limits
  • Convert to strong domain types
  • Keep domain logic working with trusted inputs
  • Return consistent validation errors
  • Log safely without leaking payloads

Input validation is how you prevent untrusted data from becoming production incidents. It is a simple practice that pays off immediately in reliability, security, and maintainability.