Data validation patterns covering schema validation, input sanitization, output encoding, and type coercion. Use for form/API validation with Zod/Pydantic/Joi/JSON Schema, XSS and injection prevention, constraint checks, data pipeline and ML feature validation.
التثبيت
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
Data validation patterns covering schema validation, input sanitization, output encoding, and type coercion. Use for form/API validation with Zod/Pydantic/Joi/JSON Schema, XSS and injection prevention, constraint checks, data pipeline and ML feature validation.
Validate untrusted data at trust boundaries before it flows into your system. This skill covers schema libraries (Zod/Pydantic/Joi/JSON Schema), coercion pitfalls, context-dependent output encoding, injection/XSS/DoS defenses, and pipeline/ML feature validation.
Core principles (read first)
Parse, don't validate. A validator that returns bool throws away work — the caller re-parses or trusts blindly. Return a typed value (Result<User>, User | errors) so downstream code cannot receive unvalidated data. Schema libraries (Zod .parse, Pydantic .model_validate) do this by construction.
Validate at the boundary, once, then trust the typed value inward. Boundaries: HTTP handlers, queue consumers, file/CLI parsers, pipeline ingestion, cross-service calls.
Server-side is authoritative; client-side validation is UX only. Never rely on it for security — attackers bypass the client entirely.
Allowlist > denylist. Enumerate what's permitted (enum, char classes, known hosts). Denylists (blocking <script>, ../, ') are always incomplete — encodings, Unicode, and case defeat them.
Canonicalize before validating. Normalize Unicode (NFC), lowercase host, resolve ./.. in paths, decode percent-encoding — then check. Validating raw input lets %2e%2e%2f or . (fullwidth) slip past.
Encoding ≠ validation. Validation decides accept/reject; encoding makes a value safe for a specific sink (HTML vs attribute vs JS vs URL vs shell vs SQL). A value can be valid and still need encoding at every sink.
Limits are validation. Cap length, array size, object depth, and total payload bytes to stop DoS (JSON bombs, deeply nested payloads, ReDoS amplification).
Zod (TypeScript)
safeParse returns a discriminated result (no throw); parse throws ZodError. Prefer safeParse at boundaries.
.strict() vs .strip() (default) vs .passthrough(): default silently drops unknown keys — safe but hides typos. .strict() rejects them (best for request bodies). Never .passthrough() untrusted input into a DB writer (overposting).
z.discriminatedUnion for tagged variants — faster and clearer errors than z.union. z.lazy for recursive schemas.
Async rules (uniqueness, DB lookups) need .parseAsync/.safeParseAsync — a sync .parse on a schema with .refine(async …) throws at runtime.
Zod validates but does not sanitize HTML — a valid string can still be an XSS payload. Encode at the sink.
⚠ Zod coercion footguns (z.coerce.* wraps the JS global constructor):
z.coerce.number() → Number(x): Number("")===0, Number(" ")===0, Number(null)===0, Number([])===0. An empty form field becomes 0, not an error. Prefer z.string().regex(/^\d+$/).transform(Number) or guard emptiness first.
z.coerce.boolean() → Boolean(x): any non-empty string is true, including "false" and "0". Use z.enum(["true","false"]).transform(v => v === "true") (or z.stringbool() in Zod 4).
HTML forms send "", never null/undefined. .optional() (accepts undefined), .nullable() (accepts null), and .default() are distinct — an empty text input is "", so add .transform(v => v || undefined) or z.literal("").or(realSchema).
⚠ v2 coercion (lax mode, the default): numeric strings coerce ("123" → 123), and float → int succeeds only with no fractional part. To reject cross-type coercion use ConfigDict(strict=True) or per-field Field(strict=True). bool in lax mode accepts "true"/"yes"/"on"/1 etc. — surprising for API inputs; use strict or Literal.
extra="forbid" on request models — default extra="ignore" silently drops unknown keys (typos pass, attacker fields ignored but not flagged).
mode="before" validators run on raw input (coerce/normalize); mode="after" run on the typed value (cross-field checks).
JSON Schema / Ajv (language-agnostic)
Use when the contract must be shared across languages or stored as data (OpenAPI, config schemas).
importAjvfrom"ajv";
import addFormats from"ajv-formats";
const ajv = newAjv({ allErrors: true, removeAdditional: true, coerceTypes: true, useDefaults: true });
addFormats(ajv);
const validate = ajv.compile(schema); // compile ONCE at startup, reuse; compiling per-request is a major perf cliffif (!validate(data)) console.log(validate.errors); // {instancePath, keyword, params, message}
⚠ Gotchas:
additionalProperties: false is not inherited and does not apply across allOf/anyOf/$ref composition — unknown keys can slip through combined schemas. Set it explicitly on each object.
coerceTypes: true mutates input ("5" → 5, "" → 0 for numbers) — same empty-string trap as JS.
removeAdditional/useDefaultsmutate the validated object in place; clone first if you need the original.
format (email, uri, date-time) requires ajv-formats; unknown formats are ignored silently unless strict: true.
Ints beyond Number.MAX_SAFE_INTEGER (2^53) lose precision — validate large IDs/money as strings (BigInt, decimal).
Empty string vs null vs absent: three distinct states. Decide per field which are allowed; don't let coercion collapse them.
Sanitization and output encoding
Encode at the sink, for the sink's context. One escaper does not fit all contexts.
importDOMPurifyfrom"dompurify"; // browser: window; server: DOMPurify(new JSDOM("").window)// Rendering user HTML (rich text): sanitize with an ALLOWLIST of tags/attrsconst clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ["b", "i", "em", "strong", "a", "p", "ul", "ol", "li"],
ALLOWED_ATTR: ["href"],
ALLOW_DATA_ATTR: false,
});
Context-dependent encoding (choose by where the value lands):
Sink
Escape
Why hand-rolling fails
HTML text
&<>"' → entities
—
HTML attribute
entity-encode + always quote
unquoted attr breaks on space//
<script> / JS string
\xHH for < > & / ' "
</script> in a JS string ends the tag
URL component
encodeURIComponent
but javascript: scheme still executes — allowlist scheme
CSS value
\HH hex (space-terminated)
expression()/url() injection
SQL
parameterized queries
escaping is not a substitute
Shell
avoid; pass argv arrays, never a string
quoting is unwinnable
⚠ Prefer framework auto-escaping (React JSX, Jinja2 autoescape, template engines) over manual encoders. Manual encoding is for the gaps (dangerouslySetInnerHTML, |safe, building HTML strings). React escapes text but nothref="javascript:…", dangerouslySetInnerHTML, or <script> content.
Never build SQL/HTML/shell by string concatenation. Parameterized queries for SQL; argv arrays for shells; DOM APIs / templating for HTML.
⚠ The naive resolved.startsWith(baseDir) check has a prefix bug: /srv/data-evil starts with /srv/data. Append the separator (as above). Decode percent-encoding before resolving.
Security-focused validation
XSS: output-encode at every sink (above); sanitize stored HTML with an allowlist; set CSP as defense-in-depth. Validation alone does not stop XSS.
Injection (SQL/NoSQL/LDAP/command): parameterize / bind; never interpolate. For NoSQL (Mongo), reject object-typed values where a scalar is expected ({"$gt": ""} operator injection).
Mass assignment / overposting:.strict() (Zod) / extra="forbid" (Pydantic) / explicit field allowlists. Never bind a request body straight onto an ORM model — an attacker sets isAdmin/role.
ReDoS: user-supplied or poorly written regexes with nested/overlapping quantifiers ((a+)+, (.*)*, (a|a)*) backtrack super-linearly → CPU DoS. Mitigate: avoid nested quantifiers, anchor patterns, cap input length before matching, and for untrusted patterns use a linear engine (Google RE2 / re2 bindings) or a match timeout. Classic offender: catastrophic email/whitespace regexes.
DoS via size/depth: enforce max body bytes (server + framework), max array length, max object nesting depth, and max string length before deep validation. JSON parsers don't bound depth by default → "billion laughs"-style expansion and stack exhaustion.
Canonicalization attacks: Unicode NFC/NFKC normalize, casefold, decode, and resolve before allowlist checks (see Core principles).
File uploads: validate by content (magic bytes / sniff), not just extension or client Content-Type (both attacker-controlled); cap size; store outside webroot; generate server-side filenames.
Data pipeline validation
import great_expectations as gx
validator = gx.from_pandas(df)
validator.expect_column_values_to_not_be_null("email")
validator.expect_column_values_to_be_unique("email")
validator.expect_column_values_to_be_in_set("status", ["active", "inactive", "pending"])
validator.expect_column_values_to_be_between("age", 0, 150)
result = validator.validate()
ifnot result.success:
raise ValueError([r for r in result.results ifnot r.success])
Validate at ingestion and fail loudly; a silently-wrong pipeline corrupts every downstream table.
Add freshness/recency checks (dbt dbt_utils.recency) — stale-but-valid data is a common silent failure.
Track row-count deltas and null-rate deltas between runs, not just absolute thresholds.
ML feature validation (train/serve consistency)
Guard against the drift that silently degrades models:
Schema parity: serving features must match training columns, dtypes, and order. A renamed/missing column often defaults to null → garbage predictions with no error.
Distribution drift: compare serving vs training via PSI or KS test per feature; alert on shift.
Categorical drift: reject/flag unseen categories (encoders map them to 0/UNK, skewing output).
Null-rate spikes and range violations (values outside training min/max) — clamp or reject per policy.
train/serve skew: the same transformation code must run in both paths; fit scalers/encoders on training data only and persist them (fitting at serve time is leakage + skew).
Checklists
Boundary validation — verify before done:
Every external input (HTTP body/query/params, headers, queue msg, file, CLI arg, cross-service payload) is parsed into a typed value at the boundary
Validator returns typed data, not a bool (parse-don't-validate)
Unknown keys rejected on request bodies (.strict() / extra="forbid") — no overposting
Length/size/array-length/depth limits set; max request-body bytes enforced at the server