Unvalidated input is behind SQL injection, XSS, business logic bypasses, and the kind of silent data corruption that surfaces months later in a support ticket. Validation is not glamorous, but getting it systematically right is one of the highest-leverage things you can do for reliability and security. Here is the 2026 approach — schema-first, layered, and automated.
What changed in 2026
- Schema libraries matured into type-system citizens. Zod (TypeScript), Pydantic v2 (Python), and Valibot generate both runtime validators and static types from a single schema definition.
- AI-assisted form generation started producing validation code that needs review — auto-generated validators sometimes miss business-rule constraints that are obvious to a human.
- Edge runtimes added validation hooks. Cloudflare Workers and Vercel Edge Functions can reject malformed requests before they hit origin.
- LLM prompt injection made output validation as important as input validation — you now need to validate what your AI returns, not just what users submit.
Validation vs sanitization
These solve different problems and should not be confused:
| Term |
What it does |
When |
| Validation |
Check that input matches expected shape/type/range |
At input boundary |
| Sanitization |
Transform input to remove dangerous content |
At output/storage context |
| Parameterization |
Treat user data as data, not code |
At query/command boundary |
Sanitizing HTML at input time (stripping tags before storage) is usually wrong — you lose data and may break legitimate content. Strip at render time. Parameterize SQL queries always.
Schema-first validation
Define the shape once; derive both runtime checks and types from it:
// TypeScript — Zod
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(254),
age: z.number().int().min(13).max(120),
role: z.enum(['admin', 'editor', 'viewer']),
website: z.string().url().optional(),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// In your handler:
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
const user = result.data; // fully typed, validated
# Python — Pydantic v2
from pydantic import BaseModel, EmailStr, field_validator
from enum import Enum
class Role(str, Enum):
admin = "admin"
editor = "editor"
viewer = "viewer"
class CreateUser(BaseModel):
email: EmailStr
age: int
role: Role
website: str | None = None
@field_validator('age')
@classmethod
def check_age(cls, v: int) -> int:
if not 13 <= v <= 120:
raise ValueError('age must be between 13 and 120')
return v
Validate at every layer
Browser form → [client schema check for UX]
↓
API gateway → [schema + rate limit]
↓
Service layer → [business rules: "can this user do this?"]
↓
DB layer → [constraints: NOT NULL, CHECK, UNIQUE]
Each layer is independent. Client validation is a UX courtesy, not a security control.
How to pick a validation approach
- TypeScript / Node? Zod is the current standard — tightly integrated with tRPC, Next.js, and Hono. Valibot is a lighter alternative if bundle size matters.
- Python? Pydantic v2 is the default; FastAPI uses it natively.
- Go?
go-playground/validator for struct tags; go-json-schema for schema-driven validation.
- REST API with OpenAPI? Define the schema in OpenAPI and generate validators — keeps docs and validation in sync.
- GraphQL? Input type definitions give you validation; add custom scalars for formats like email and URL.
Common mistakes
Validating only on the client. Attackers don't use your frontend. The server must always re-validate, period.
Trusting Content-Type headers. Parse and validate the body regardless of what Content-Type claims; a mis-declared type is trivial to forge.
Rejecting too aggressively. Names with apostrophes, addresses with commas, Unicode usernames — all valid. Validate structure and length, not character whitelists unless you have a real reason.
Missing nested validation. Validate deeply nested objects, not just top-level keys. A missing check on user.address.zipCode is just as exploitable.
Swallowing validation errors. Return structured errors that tell users exactly what to fix. Logs that just say "validation failed" are useless for debugging.
What to skip
- Hand-rolled regex for email addresses — use a library validator that handles the RFC edge cases.
- Stripping all HTML at input if you store rich content — sanitize at render with DOMPurify or equivalent.
- Storing unvalidated data "for later" processing — validate before storage, always.
FAQ
Should I validate query parameters the same way as request bodies?
Yes. Query strings are user input. Parse them through a schema; never use raw string values in DB queries or business logic.
How do I handle validation in background jobs that process queued messages?
Validate at both enqueue time (producer) and dequeue time (consumer) — message queues are not a trust boundary, and schema drift is real.
What about validating AI-generated output?
Use the same schema tools. Define the expected shape, parse the model's response with safeParse, and handle failures explicitly rather than assuming the LLM returned valid JSON.
How strict should I be with extra fields?
Strip unknown fields by default (z.object().strict() or Pydantic's model_config = {"extra": "forbid"}). Mass assignment vulnerabilities happen when extra fields are silently passed through to the ORM.
Where to go next
See How to handle errors gracefully in 2026, Error handling explained in 2026, and How to write unit tests in 2026.