--- name: validating-input description: Validates and sanitizes incoming request data at the service boundary — schema validation, type/length/range/format checks, allowlists, unknown-field rejection, and structured 422 error responses. Use when the user asks to validate request bodies, query params, headers, or uploads, add a pydantic/zod/joi schema, prevent malformed or malicious payloads, or fix a path-traversal or oversized-input issue. Do not use for login and permission decisions (implementing-authentication, implementing-authorization) or for business rules deeper than the request boundary. --- # Validating Input ## When to use / when NOT to use - **Use for:** request-boundary validation of bodies, params, headers, and file/path inputs; schema definitions; validation error responses. - **Do NOT use for:** authn/authz decisions, domain/business invariants (those live in the domain layer), or output encoding (XSS is an output concern — encode at render time). ## Core rules 1. **Validate at the boundary with the stack's schema library** — pydantic (Python), zod (TS), joi (Node legacy). Hand-rolled `if` chains drift. 2. **Every field gets type + length + range + format.** Unbounded strings and numbers are bugs. - ✅ `name: str, min_length=1, max_length=200` - ❌ `name: str` — a 10 MB name is now valid 3. **Allowlist over blocklist.** Enumerate what is valid (enum, regex anchored `^...$`, closed set); never try to enumerate evil. 4. **Reject unknown fields** (`extra="forbid"` / `.strict()`). Silent extras become mass-assignment holes. 5. **Canonicalize before validating:** trim whitespace, NFC-normalize unicode, lowercase emails — then check. Validating pre-canonical data lets `admin ` ≠ `admin` bypasses through. 6. **Client-side validation is UX only.** The server re-validates everything, always. 7. **Fail with a structured 422** naming every invalid field and why — one pass, not first-error-only. 8. **Path and file inputs:** resolve to an absolute path and require it start with the allowed base directory; validate content-type by sniffing magic bytes, not the filename; cap upload size before reading the body. ## Workflow 1. Define one schema per endpoint request (body, query, path params) with rule-2 constraints on every field. 2. Enable unknown-field rejection and canonicalization hooks (rules 4–5). 3. Wire the validation-error handler to the structured 422 format (rule 7). 4. Add path/upload guards where files are involved (rule 8). 5. **Validate the validator:** send a malformed payload per field class (wrong type, over-length, out-of-range, unknown field, path `../../etc/passwd`) and confirm each yields a 422/400 naming the field — never a 500. Fix and repeat until all pass. ## Edge cases & failure modes - **Numbers as strings** (`"42"`) → decide once: coerce (default for query params) or reject (default for JSON bodies); stay consistent. - **Empty vs missing vs null** → distinguish explicitly in the schema; PATCH semantics need "absent = unchanged". - **Arrays** → cap length (e.g. ≤1000 items) and validate every element, not just the container. - **Validation library throws on deeply nested payloads** → cap request size and nesting depth at the web-server layer first. ## References Copy-paste patterns and gotchas: see [references/patterns.md](references/patterns.md).