Use Zod 4 idiomatically in TypeScript projects for schema validation, type inference, serialization/deserialization, JSON Schema interop, and error handling. Trigger whenever the user mentions Zod, schema validation in TypeScript, runtime type checking, form validation, API contract validation, z.object, z.string, z.infer, z.parse, or is writing TypeScript code that involves parsing, validating, or transforming untrusted data. Also trigger when migrating from Zod 3 to Zod 4, or when the user references Zod schemas in the context of OpenAPI, JSON Schema, tRPC, React Hook Form, or similar integrations. This skill covers Zod 4.x (including 4.1 codecs, 4.3 features) and explicitly flags Zod 3 assumptions that no longer hold.
Zod 4 is the current stable release (latest: 4.3.x). It ships three packages from a single
zod install: zod (classic), zod/mini (tree-shakable), and zod/v4/core (for library
authors). TypeScript ≥5.5 and strict: true in tsconfig are hard requirements.
npm install zod@^4.0.0
Import the classic API throughout unless bundle size constraints demand zod/mini:
import * as z from "zod";
Navigating this skill
§1–§4 cover daily usage: schemas, parsing, error handling, type inference. §5 covers the Zod 3 → 4 migration traps — read this if porting existing code. §6–§8 cover powerful but less frequently needed features:
- §6 Codecs — bidirectional serialization (API boundaries, date handling)
z.xor, branded types, zod/mini, library authoring via zod/v4/coreFor codec recipes, JSON Schema options, and zod/mini API mappings, see the
references/ directory.
Prefer top-level format constructors over the deprecated method chain. This is both more tree-shakable and the direction of the API going forward (method equivalents will be removed in Zod 5).
// ✅ Zod 4 idiomatic
z.email()
z.uuidv4()
z.url()
z.ipv4()
z.iso.datetime()
// ❌ Deprecated — still works, will be removed in next major
z.string().email()
z.string().uuid()
All standard primitives: z.string(), z.number(), z.bigint(), z.boolean(),
z.date(), z.null(), z.undefined(), z.void(), z.symbol(), z.never(),
z.any(), z.unknown(). Note: z.literal() no longer accepts symbols.
z.literal() now accepts arrays for multi-value literals:
const HttpOk = z.literal([200, 201, 204]); // 200 | 201 | 204
Fixed-width types with built-in range constraints:
z.int() // safe integers only
z.int32() // [-2^31, 2^31-1]
z.uint32() // [0, 2^32-1]
z.float32() // single-precision range
z.float64() // double-precision range
z.int64() // ZodBigInt — exceeds safe integer range
z.uint64() // ZodBigInt
const User = z.object({
name: z.string(),
age: z.int(),
email: z.email(),
});
type User = z.infer<typeof User>;
For strict (reject unknown keys) or loose (pass-through unknown keys) objects, use the
top-level constructors rather than the deprecated .strict() / .passthrough() methods:
z.strictObject({ name: z.string() }); // rejects unrecognized keys
z.looseObject({ name: z.string() }); // passes through unrecognized keys
z.object({ name: z.string() }); // strips unrecognized keys (default)
Use .extend() or shape spread. .merge() is deprecated.
const WithName = Base.extend({ name: z.string() });
// best tsc performance — use shape spread:
const WithName2 = z.object({ ...Base.shape, name: z.string() });
// .safeExtend() — preserves refinements and enforces extends constraint (4.1+)
const WithAge = Base.safeExtend({ age: z.int() });
⚠ Zod 3 trap: .extend() on a refined schema now throws if you overwrite existing
properties. Use .safeExtend() to add new properties preserving refinements, or rebuild
from .shape.
z.nativeEnum() is deprecated. z.enum() now handles both string arrays and TypeScript