Zod Schema
Use this capability whenever a change touches Zod — a schema being written, a parse being placed, a payload being decoded, or an error being surfaced. It owns the schema layer: the shape, the parse call, the type that shape yields, the error it produces, and the codec that carries data between a wire format and a domain model.
It does not own four neighbouring questions, each owned by a capability of its own:
- Whether an input is untrusted at all, and so must be checked — an application-security capability's judgment. This skill assumes that judgment is made and says how Zod carries it out.
- That a hook exists for the parse to sit inside — a route handler, a server function, a form action — a framework capability's fact.
- When a fetched payload is refetched, cached, or invalidated — a server-state capability's concern. This skill only puts a parse inside the query function it owns.
- How a component receives, renders, and is tested against the parsed result — a component-development capability's concern.
Where a rule here has a counterpart in one of those, this skill states the Zod mechanism and names the other as owner.
Version discipline. This skill is written against Zod 4 only. Zod 3 is out of scope as a supported line: no migration path, no zod/v3 guidance, and no rule stated in its Zod 3 form. The one place Zod 3 appears is as a detection signal — a list of superseded idioms, so stale code can be recognised and replaced with the current form. That is a Zod 4 rule about a Zod 4 codebase, not coverage of Zod 3. The boundary matters more than usual here, because Zod 3 answers still outrank Zod 4 answers across tutorials, forum posts, and model recall — and most Zod 3 idioms still compile against a Zod 4 install, so "it runs" is not evidence a rule is current. Zod 4 also moves within its own major: z.xor, z.invertCodec, and the .brand direction parameter all arrived after 4.0. Every version-sensitive statement here names what it was verified against, and where a surface is known to move the rule is a lookup — consult the installed version's own documentation at https://zod.dev/ — rather than a frozen API name. Treat an unversioned claim about a Zod API, in this skill or anywhere else, as suspect.
Verified against [email protected], published 2026-05-04 — https://zod.dev/ — with TypeScript 5.5+ and strict: true as stated prerequisites. Each reference file carries its own Verified against line with the upstream page its rules were checked against.
Out of scope. Zod 3 in every form. The 4.5.0-canary line, which no rule here was derived against. Other validation libraries except where a boundary is shared with one — Standard Schema interop is covered, Valibot's or ArkType's own APIs are not. Code generation tools that emit Zod schemas are named where they change what you must still write by hand, but their own configuration is theirs.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
The Validation Boundary
See validation-boundary.md for:
- placing the one parse at the edge data enters, and never re-parsing inward of it
unknownupstream of the parse andz.infer<typeof Schema>downstream, never a hand-written interface- which questions belong to an application-security, framework, server-state, or component capability
- the cases a schema costs without buying — internal data, an already-parsed value, a per-item loop
- running Zod beside a second validator, and converting between them at one place
Version and Packages
See version-and-packages.md for:
- telling
zod.dev(Zod 4) fromv3.zod.dev(Zod 3) before trusting a page - picking between
zod,zod/mini,zod/locales, and thezod/v4/*permalinks - the assumed TypeScript 5.5+ and
strict: true, without which inferred optionality is unsound - the minor-version floors for
z.xor,z.invertCodec,.safeExtend,z.property, and the.branddirection - the Zod 3 idioms that still compile against Zod 4, as a staleness signal
Schema Modules
See schema-modules.md for:
- the
const X/type XandzXnaming conventions, and picking one per repository - shared
EntityId,Timestamp, andNonEmptyStringprimitives as the vocabulary other schemas compose from .brand<"Name">(), its"out"/"in"/"inout"direction, and the fixtures it forces through a parse- deriving with
.shape,.extend,.safeExtend,.pick,.omit, and.merge()'s 4.4 throw on refinements - hoisting every schema to module scope rather than rebuilding it per request or render
Primitives and Formats
See primitives-and-formats.md for:
- the top-level
z.email(),z.url(),z.uuid(), andz.iso.datetime()that replaced thez.string().x()methods - choosing strict
z.uuid()over permissivez.guid(), andz.httpUrl()overz.url()for a fetched URL z.stringFormat()for a project's own format andz.templateLiteral()for a template-literal typez.int()'s safe-integer range, the rejection ofInfinity, and the fixed-widthz.int32()andz.float64()z.enum()over the deprecatedz.nativeEnum(), with.exclude()and.extract()
Objects and Collections
See objects-and-collections.md for:
z.object()stripping unmodelled keys, andz.strictObject(),z.looseObject(), and.catchall()changing that- self-reference through a getter, and the return-type annotation a circularity error needs
z.record()'s two required arguments, exhaustive enum keys, andz.partialRecord()z.tuple()where.nonempty()no longer yields a tuple type, plusz.file()andz.json()- parsing an array schema once instead of an item schema per element
Unions
See unions.md for:
z.discriminatedUnion()for narrowing, error quality, and parse speed over a plainz.union()- the literal discriminator every branch must fix, and the nested unions and pipes Zod 4 allows inside one
z.xor()for the case where a value matching two branches is a defectz.intersection()throwing a plainErrorrather than aZodErroron an unmergeable result- the union of optional-field shapes that should have been one object
Optionality and Defaults
See optionality-and-defaults.md for:
- choosing
.optional(),.nullable(), or.nullish()from what the producer actually emits .default()short-circuiting on the output type versus.prefault()parsing on the input type.catch()turning a parse failure into a fallback, and when that hides a drifting producer.nullish().transform((v) => v ?? null)to collapse a mixed null-and-undefined producer- what
exactOptionalPropertyTypesdoes to an optional property's inferred type
Refinements
See refinements.md for:
.refine(),.superRefine(), and.check(), and the rule that none of them may throwctx.addIssue(),z.NEVER, and thepaththat lands an error on a specific fieldabortto stop a chain, and 4.4'swhento skip a check whose inputs are already invalid- an async refinement forcing
.parseAsync()on every existing caller of the composed schema - a permission, quota, or uniqueness rule belonging in the domain rather than the schema
Transforms and Pipes
See transforms-and-pipes.md for:
.transform()after validation,z.preprocess()before it, and.pipe()between two schemas.overwrite()where the value changes but the type and its introspectability must notz.inputversusz.output/z.infer, and the form and fixture bugs from confusing themctx.issues.pushor 4.4'sctx.addIssuewithz.NEVERto fail from inside a transform- where reshaping the producer's data ends and a domain decision begins
Codecs
See codecs.md for:
z.codec(input, output, { decode, encode })for a boundary.transform()cannot reverse.decode()and.encode()with theirsafe*and*Asyncforms, plusz.invertCodec()- the documented
isoDatetimeToDate,json,stringToURL, andbase64ToBytestemplates — copied, not imported - defaults and
.catch()applying on decode only, and a.transform()making encode throw a non-ZodError - the total codec that cannot fail, and the shape guarantee it therefore does not give
Coercion and Configuration
See coercion-and-config.md for:
z.coerce.number().parse("")returning0, andz.coerce.boolean().parse("false")returningtruez.stringbool()as the construct that rejects a value in neither the truthy nor the falsy set- query strings, form data, and environment variables as the three string-valued sources
- parsing configuration once at startup and terminating rather than surfacing
undefinedlater - keeping secret configuration out of logs and out of a client bundle
Parsing
See parsing.md for:
.parse()when a failure is a defect,.safeParse()when it is an outcome to branch on.parseAsync()being mandatory rather than optional once any check in the schema is async- the deep clone a parse returns, and the reference identity it therefore breaks
- remapping
ZodErrorto a domain error so Zod stays out of domain signatures - logging the boundary and the issue paths, never the input that failed
Errors
See errors.md for:
error.issueswith itscodeandpath, and the dropped.errorsand.formErrorsaliasesz.treeifyError(),z.flattenError(), andz.prettifyError(), chosen by consumer- the unified
errorparameter that replacedmessage,invalid_type_error, anderrorMap - the precedence — schema, per-parse,
z.config({ customError }), locale — that reversed in Zod 4 reportInputputting untrusted input into an error that travels to a tracker or a client
Metadata and JSON Schema
See metadata-and-json-schema.md for:
.meta(),z.registry(),z.globalRegistry, and extendingGlobalMetaby declaration merging- metadata binding to a schema instance, so
.optional()or.extend()orphans it z.toJSONSchema()'starget,io,unrepresentable,cycles,reused, andurioptionsio: "input"producing a different document from the default output view- the
date,map,set,transform, andbiginttypes with no JSON Schema representation
Model Structured Output
See model-structured-output.md for:
- a schema as a
generateObjector tool contract, andsatisfies z.ZodRawShapewhere a raw shape is wanted .describe()text reaching the model as instruction rather than as documentation.nullable()surviving a provider's strict mode where.optional()and.nullish()are rejected- reusing
.shape.fieldso model output cannot drift from the domain type it feeds - re-prompting from a
ZodErrorunder a bounded retry, rather than patching invalid output
Forms
See forms.md for:
- a Zod-specific resolver for Zod's own issue detail, or a Standard Schema one for library independence
z.inputfor the form's values and default values,z.outputfor the submit handlerpathon a cross-field refinement as the only way an error reaches the right input- the empty string a blank numeric field submits, and the absent key an unchecked checkbox sends
- the server-side re-parse that a client-side schema never replaces
Data Store Boundaries
See data-store-boundaries.md for:
- the stored shape and the domain model as two schemas with a transform between them
- read and write halves written as one pair, so a renamed field cannot drift between them
.partial()for a patch and.omit().extend()for a create input carrying store sentinelsz.instanceof()for a driver's own types andz.property()to constrain one of their fields- a payload varying by query depth, draft state, or locale — and the all-optional schema that is not the answer
Performance and Footprint
See performance-and-footprint.md for:
- the published ~14x, ~7x, and ~6.5x parse gains, and the ~25,000-to-~175 type-instantiation drop
zod/mini's functional API and 1.88 kB core, and why a server should not adopt it- schema construction hoisted out of a hot path, and a collection parsed once rather than per item
.check()over.refine()and a discriminated union over a linear one, on a measured pathz.looseObject()skipping the key-stripping pass, and the protection that trades away
Security Posture
See security-posture.md for:
z.object()'s stripping as mass-assignment protection, and whatz.looseObject()re-admits- a passing parse being neither HTML escaping, SQL parameterization, nor a safe-fetch guarantee
.max()placed before a.regex()or an expensive refinement, to bound attacker-controlled work- unbounded arrays, unbounded recursion depth, and cyclical input that loops forever
- an allowlisting response schema, without which a private field leaves with the payload
Testing Schemas
See testing-schemas.md for:
- why asserting that
z.email()rejects"nope"tests Zod rather than the schema z.input<typeof Schema>as the fixture type, built from an observed payload rather than the schema- asserting a rejection through
.safeParse().successor.toThrow(), and onpath/codenot the message - round-tripping through the codec's own
.decode()and.encode()rather than re-implementing either - generated data being unable to show that the schema itself is wrong
Interop and Library Code
See interop-and-library-code.md for:
<S extends z.ZodType>preserving the caller's schema where a barez.ZodType<T>loses it- Standard Schema's
~standardfor a consumer that must not care which library produced the schema zod/v4/corewithzodas a^4.0.0peer dependency for library code._zod.defand the$Zod*types as an explicitly unstable surface