| Situation | Use |
|---|---|
| Default / most apps | zod |
| Strict frontend bundle-size budget | zod/mini |
| Building a library on top of Zod | import only "zod/v4/core" |
| Need both Zod 3 & 4 support | peer "^3.25.0 || ^4.0.0", import "zod/v3" + "zod/v4/core" |
| Need | Use |
|---|---|
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) |
| Need | Use |
|---|---|
| Strip unknown keys (default) | z.object({...}) |
| Reject unknown keys | z.strictObject({...}) |
| Pass unknown keys through | z.looseObject({...}) |
| Validate unknown keys against a schema | .catchall(schema) |
| Shape | Use |
|---|---|
| Any option may match | z.union([...]) |
| Object variants sharing one literal tag | z.discriminatedUnion("key", [...]) |
| Exactly one option must match | z.xor([...]) (use .strict() on object options to avoid false overlap) |
| Need | Use |
|---|---|
| Arbitrary string keys | z.record(z.string(), valueSchema) |
| Enum keys, all required | z.record(enumSchema, valueSchema) |
| Enum keys, some optional | z.partialRecord(enumSchema, valueSchema) |
| Known field + arbitrary extra fields | .and(z.looseRecord(...)) |
| From | To | Use |
|---|---|---|
| coerce, don't care about precision | any | z.coerce.string()/.number()/.boolean() |
| "truthy word" → boolean, precisely | boolean | z.stringbool() (not z.coerce.boolean()) |
| ISO string ↔ Date, both directions | Date | z.codec(z.iso.datetime(), z.date(), {...}) |
| one-way string → derived value | anything | .transform(fn) |
check-level → schema-level ({error:...}) → per-parse (.parse(x,{error})) → global (z.config({customError})) → locale
z.discriminatedUnion() over z.union() whenever every option is an object sharing one literal key — faster, better errors..safeParse() over try/catch + .parse() in any path where invalid input is expected/common, not exceptional..prefault(), not .default(), when the fallback value should go through the same transforms/refinements as real input.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.{ ...A.shape, ...B.shape }), not chained .extend(), when merging more than two schemas — avoids quadratic tsc cost..safeExtend() instead of .extend() on any schema that carries a .refine() — plain .extend() throws on refined schemas.throw inside .refine()/.transform() — return falsy / push to ctx.issues + return z.NEVER instead; thrown errors aren't caught by Zod.z.compile() — methods like .refine()/.extend() return a new, uncompiled schema.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.