{"post":{"slug":"how-to-validate-user-input-2026","title":"How to validate user input in 2026: schemas, sanitization, and defence in depth","excerpt":"Unvalidated input is the root cause of injection attacks, data corruption, and silent bugs. This is the 2026 guide to validation at every layer of your stack.","category":"code","date":"March 18, 2026","author":"ByteLedger Team","content":"<span class=\"dropcap\">U</span>nvalidated 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.\n\n## What changed in 2026\n\n- **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.\n- **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.\n- **Edge runtimes added validation hooks.** Cloudflare Workers and Vercel Edge Functions can reject malformed requests before they hit origin.\n- **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.\n\n## Validation vs sanitization\n\nThese solve different problems and should not be confused:\n\n| Term | What it does | When |\n| --- | --- | --- |\n| **Validation** | Check that input matches expected shape/type/range | At input boundary |\n| **Sanitization** | Transform input to remove dangerous content | At output/storage context |\n| **Parameterization** | Treat user data as data, not code | At query/command boundary |\n\nSanitizing 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.\n\n## Schema-first validation\n\nDefine the shape once; derive both runtime checks and types from it:\n\n```typescript\n// TypeScript — Zod\nimport { z } from 'zod';\n\nconst CreateUserSchema = z.object({\n  email: z.string().email().max(254),\n  age: z.number().int().min(13).max(120),\n  role: z.enum(['admin', 'editor', 'viewer']),\n  website: z.string().url().optional(),\n});\n\ntype CreateUserInput = z.infer<typeof CreateUserSchema>;\n\n// In your handler:\nconst result = CreateUserSchema.safeParse(req.body);\nif (!result.success) {\n  return res.status(400).json({ errors: result.error.flatten() });\n}\nconst user = result.data; // fully typed, validated\n```\n\n```python\n# Python — Pydantic v2\nfrom pydantic import BaseModel, EmailStr, field_validator\nfrom enum import Enum\n\nclass Role(str, Enum):\n    admin = \"admin\"\n    editor = \"editor\"\n    viewer = \"viewer\"\n\nclass CreateUser(BaseModel):\n    email: EmailStr\n    age: int\n    role: Role\n    website: str | None = None\n\n    @field_validator('age')\n    @classmethod\n    def check_age(cls, v: int) -> int:\n        if not 13 <= v <= 120:\n            raise ValueError('age must be between 13 and 120')\n        return v\n```\n\n## Validate at every layer\n\n```\nBrowser form  →  [client schema check for UX]\n                     ↓\nAPI gateway   →  [schema + rate limit]\n                     ↓\nService layer →  [business rules: \"can this user do this?\"]\n                     ↓\nDB layer      →  [constraints: NOT NULL, CHECK, UNIQUE]\n```\n\nEach layer is independent. Client validation is a UX courtesy, not a security control.\n\n## How to pick a validation approach\n\n1. **TypeScript / Node?** Zod is the current standard — tightly integrated with tRPC, Next.js, and Hono. Valibot is a lighter alternative if bundle size matters.\n2. **Python?** Pydantic v2 is the default; FastAPI uses it natively.\n3. **Go?** `go-playground/validator` for struct tags; `go-json-schema` for schema-driven validation.\n4. **REST API with OpenAPI?** Define the schema in OpenAPI and generate validators — keeps docs and validation in sync.\n5. **GraphQL?** Input type definitions give you validation; add custom scalars for formats like email and URL.\n\n## Common mistakes\n\n**Validating only on the client.** Attackers don't use your frontend. The server must always re-validate, period.\n\n**Trusting Content-Type headers.** Parse and validate the body regardless of what Content-Type claims; a mis-declared type is trivial to forge.\n\n**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.\n\n**Missing nested validation.** Validate deeply nested objects, not just top-level keys. A missing check on `user.address.zipCode` is just as exploitable.\n\n**Swallowing validation errors.** Return structured errors that tell users exactly what to fix. Logs that just say \"validation failed\" are useless for debugging.\n\n## What to skip\n\n- **Hand-rolled regex for email addresses** — use a library validator that handles the RFC edge cases.\n- **Stripping all HTML at input** if you store rich content — sanitize at render with DOMPurify or equivalent.\n- **Storing unvalidated data \"for later\" processing** — validate before storage, always.\n\n## FAQ\n\n**Should I validate query parameters the same way as request bodies?**\nYes. Query strings are user input. Parse them through a schema; never use raw string values in DB queries or business logic.\n\n**How do I handle validation in background jobs that process queued messages?**\nValidate at both enqueue time (producer) and dequeue time (consumer) — message queues are not a trust boundary, and schema drift is real.\n\n**What about validating AI-generated output?**\nUse 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.\n\n**How strict should I be with extra fields?**\nStrip 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.\n\n## Where to go next\n\nSee [How to handle errors gracefully in 2026](/blog/how-to-handle-errors-gracefully-2026), [Error handling explained in 2026](/blog/error-handling-explained-2026), and [How to write unit tests in 2026](/blog/how-to-write-unit-tests-2026).","hue":314,"metaTitle":"How to Validate User Input in 2026: Schemas and Sanitization","metaDescription":"Validate user input correctly in 2026 — schema libraries, sanitization, defence in depth, and the mistakes that lead to injection and data corruption.","keywords":["how to validate user input 2026","input validation best practices","zod schema validation","pydantic validation python","sanitize user input","prevent sql injection 2026","server side validation","form validation 2026","input validation vs sanitization","defence in depth validation"],"tldr":["**Validate at the boundary** — the moment data enters your system, before it touches business logic or storage.","**Use a schema library** (Zod, Pydantic, Joi) rather than hand-rolling checks — you get types, error messages, and coercion for free.","**Sanitize for output context**, not input — strip HTML at render time, parameterize queries at DB time.","**Validate on both client and server** — client validation is UX, server validation is security.","**Skip:** trusting that your frontend already validated it — the server must always re-validate independently."],"readTime":"4 min read","wordCount":872},"related":[{"slug":"how-to-parse-json-2026","title":"How to parse JSON in 2026: Python, validation, and large-file tricks","excerpt":"JSON parsing in Python is a one-liner until the data is malformed, huge, or deeply nested. This 2026 guide covers the standard library, schema validation, streaming, and the fastest libraries for large payloads.","category":"code","date":"April 16, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":311},{"slug":"environment-variables-explained-2026","title":"Environment variables explained in 2026: config done right","excerpt":"Environment variables are the standard way to configure software across environments, but most teams manage them poorly. Here is the 2026 guide to doing it right.","category":"code","date":"March 10, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":308},{"slug":"what-is-a-git-hook-2026","title":"What Is a Git Hook in 2026? Automate Your Workflow","excerpt":"A git hook is a script that git runs automatically at a specific point, like before a commit or before a push. Here is what git hooks are, what they are good for, and how to set one up in 2026.","category":"code","date":"July 17, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":320}],"sameCategory":[{"slug":"bazel-build-explained-2026","title":"Bazel Explained: What Hermetic Builds Actually Cost","excerpt":"Bazel delivers reproducible, precisely cached builds and demands that you describe everything. Here is what you get, what it costs, and who should say no.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":145},{"slug":"graphql-federation-2026","title":"GraphQL Federation: One Graph, Many Teams","excerpt":"Federation composes independently-owned subgraphs into a single API. Here is what it solves, the operational cost it adds, and when a simpler gateway is enough.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":300},{"slug":"hermetic-builds-explained-2026","title":"Hermetic Builds Explained: Same Inputs, Same Output","excerpt":"A hermetic build depends only on declared inputs. Here is what that property gives you, the common leaks that break it, and how to move toward it incrementally.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":130},{"slug":"monorepo-tooling-compared-2026","title":"Monorepo Tooling Compared: What You Actually Need","excerpt":"Monorepo tools solve task orchestration and caching, not repository layout. Here is what each tier provides and how to tell when you have outgrown the simple option.","category":"code","date":"August 19, 2026","author":"ByteLedger Team","readTime":"3 min read","hue":150}],"newer":{"slug":"how-to-use-sora-2026","title":"How to use Sora in 2026: video generation, prompting, and what it actually does well","excerpt":"Sora moved from waitlist to widely available in 2026, but most users still misuse it. Here is what it does well, its real limits, and how to prompt for usable video.","category":"ai","date":"March 18, 2026","author":"ByteLedger Team","readTime":"4 min read","hue":208},"older":{"slug":"how-to-wake-up-early-2026","title":"How to wake up early in 2026: fix the morning by fixing the evening","excerpt":"Waking up early is a sleep architecture problem, not a willpower problem. Get the evening right and early mornings follow. Here is the exact protocol.","category":"guides","date":"March 18, 2026","author":"ByteLedger Team","readTime":"5 min read","hue":300}}