Cheatsheet

Cheatsheet

Package choice

SituationUse
Default / most appszod
Strict frontend bundle-size budgetzod/mini
Building a library on top of Zodimport only "zod/v4/core"
Need both Zod 3 & 4 supportpeer "^3.25.0 || ^4.0.0", import "zod/v3" + "zod/v4/core"

Absence semantics

NeedUse
Key may be undefined.optional()
Key may be null.nullable()
Key may be undefined or null.nullish()
Key may be absent, never explicit undefined.exactOptional()
Fallback when input is undefined, skip pipeline.default(value)
Fallback when input is undefined, still run pipeline.prefault(value)
Fallback on any validation failure.catch(value)

Object strictness

NeedUse
Strip unknown keys (default)z.object({...})
Reject unknown keysz.strictObject({...})
Pass unknown keys throughz.looseObject({...})
Validate unknown keys against a schema.catchall(schema)

Union-shaped data

ShapeUse
Any option may matchz.union([...])
Object variants sharing one literal tagz.discriminatedUnion("key", [...])
Exactly one option must matchz.xor([...]) (use .strict() on object options to avoid false overlap)

Dictionaries

NeedUse
Arbitrary string keysz.record(z.string(), valueSchema)
Enum keys, all requiredz.record(enumSchema, valueSchema)
Enum keys, some optionalz.partialRecord(enumSchema, valueSchema)
Known field + arbitrary extra fields.and(z.looseRecord(...))

String→type conversion

FromToUse
coerce, don't care about precisionanyz.coerce.string()/.number()/.boolean()
"truthy word" → boolean, preciselybooleanz.stringbool() (not z.coerce.boolean())
ISO string ↔ Date, both directionsDatez.codec(z.iso.datetime(), z.date(), {...})
one-way string → derived valueanything.transform(fn)

Error message precedence (highest → lowest)

check-level → schema-level ({error:...}) → per-parse (.parse(x,{error})) → global (z.config({customError})) → locale

Decision rules

  • Use z.discriminatedUnion() over z.union() whenever every option is an object sharing one literal key — faster, better errors.
  • Use .safeParse() over try/catch + .parse() in any path where invalid input is expected/common, not exceptional.
  • Use .prefault(), not .default(), when the fallback value should go through the same transforms/refinements as real input.
  • Use z.instanceof() for class instances (including built-ins like URL/RegExp), z.templateLiteral() for template-literal-typed strings, z.custom<T>() only as the last resort — always with a real validator function.
  • Use object spread ({ ...A.shape, ...B.shape }), not chained .extend(), when merging more than two schemas — avoids quadratic tsc cost.
  • Use .safeExtend() instead of .extend() on any schema that carries a .refine() — plain .extend() throws on refined schemas.
  • Never throw inside .refine()/.transform() — return falsy / push to ctx.issues + return z.NEVER instead; thrown errors aren't caught by Zod.
  • Compile last when using z.compile() — methods like .refine()/.extend() return a new, uncompiled schema.

Performance & tooling

  • AOT compile hot paths with z.compile(schema) (canary-only) — biggest wins on wide objects and large arrays, none on a bare z.string().
  • z.toJSONSchema(schema) for a single self-contained JSON Schema; z.toJSONSchema(registry) for interlinked $ref-based multi-schema output — every schema in the registry needs a registered id.