Input Validation Basics
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.