When to use: whenever a runtime-validated type and its TypeScript type must stay in sync.
How: define the schema, then derive type X = z.infer<typeof X> — never hand-write a parallel interface.
Trade-offs: the schema becomes the single source of truth; if input/output diverge (transforms, codecs), use z.input<>/z.output<> instead of z.infer<> alone.
When to use: parsing at a network/storage boundary where the same shape must later be serialized back (ISO string ↔ Date, JSON string ↔ object).
How: z.codec(inputSchema, outputSchema, { decode, encode }); .parse()/.decode() go forward, .encode() goes backward, z.invertCodec() flips direction.
Trade-offs: more setup than a one-way .transform(), but the only correct choice when the value must round-trip; .transform() anywhere in a schema makes .encode() throw.
When to use: writing a function that accepts "any Zod schema" without losing its specific subclass.
How: function f<T extends z4.$ZodType>(schema: T), never (schema: z4.$ZodType<T>); narrow further with <T extends z4.$ZodObject> or <T extends z4.$ZodType<SomeOutput>> as needed.
Trade-offs: slightly more verbose signatures, but preserves caller type information that a naive generic parameter would erase.
.extend()When to use: merging multiple object schemas, especially in a chain.
How: z.object({ ...A.shape, ...B.shape, extra: z.string() }) instead of A.extend({...}).extend({...}).
Trade-offs: identical behavior in Zod and Zod Mini, avoids the quadratic tsc cost of chained .extend(), but loses .extend()'s automatic strictness-level inheritance — pick z.strictObject()/z.looseObject() explicitly.
When to use: a union of object shapes that all share one literal "kind"/"status"/"type" field.
How: z.discriminatedUnion("status", [objA, objB, ...]) instead of z.union([...]).
Trade-offs: faster parsing and better error messages than a naive union; requires every option to be an object schema with the same literal-valued key.
When to use: a Record<K, V> keyed by a fixed set of literal/enum values.
How: z.record(enumKeys, valueSchema) for "every key must be present" (matches TS Record<K,V>); z.partialRecord(enumKeys, valueSchema) when only some keys may appear.
Trade-offs: defaulting to z.record() on enum keys silently requires every key — a common migration surprise for anyone expecting partial dictionaries.
When to use: building consistent, localized, field-aware error messages across an application.
How: set a locale globally (z.config(en())) as the baseline, add a global customError map for app-wide conventions, override per-schema with the error param for field-specific messages, and reach for per-parse error maps only for one-off call-site overrides.
Trade-offs: precedence runs check > schema > per-parse > global > locale — forgetting this order leads to "why isn't my override working" bugs when a more specific layer already set a message.
When to use: schemas that need to double as OpenAPI/JSON Schema definitions or AI structured-output specs.
How: attach .meta({ id, title, description }) (registers in z.globalRegistry) at schema-definition time, then call z.toJSONSchema(schema) (single schema) or z.toJSONSchema(z.globalRegistry) (interlinked, $ref-based multi-schema output) when needed.
Trade-offs: metadata fields override Zod's generated JSON Schema keywords, which is powerful but means metadata mistakes propagate directly into the generated spec.
When to use: self-referential or mutually-recursive object types (trees, linked records).
How: define the recursive field as get field() { return SchemaThatReferencesMe } instead of a plain property.
Trade-offs: works reliably only for object-referencing-object patterns; mixing in unions or nested function calls inside the getter often breaks TypeScript's inference and needs an explicit return-type annotation.
When to use: default to full zod; switch to zod/mini only under measured, strict frontend bundle-size constraints.
How: both packages implement identical behavior against the shared zod/v4/core; library code that must support both builds exclusively against "zod/v4/core" types.
Trade-offs: Mini's functional .check()-based API is more verbose and less autocomplete-friendly; the bundle savings are real (~60-70% smaller) but usually irrelevant on backends or over typical network latency.