name: zod4-typescript description: > 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 for TypeScript
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)
- §7 Metadata, registries, and JSON Schema — OpenAPI generation, schema documentation
- §8 Advanced patterns — recursive types, template literals, discriminated unions,
z.xor, branded types,zod/mini, library authoring viazod/v4/coreFor codec recipes, JSON Schema options, and
zod/miniAPI mappings, see thereferences/directory.
§1 Defining schemas
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()
Primitives and literals
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
Numeric formats
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
Objects
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)
Extending and composing objects
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.
Enums
z.nativeEnum() is deprecated. z.enum() now handles both string arrays and TypeScript
enums:
const Status = z.enum(["active", "inactive", "suspended"]);
// TypeScript enum (not recommended, but supported)
enum Direction { Up = "UP", Down = "DOWN" }
const Dir = z.enum(Direction);
Access values via .enum (.Enum and .Values are removed):
Status.enum.active; // "active"
Records
z.record() now requires two arguments (key schema, value schema). The single-argument
form is removed.
z.record(z.string(), z.number()); // Record<string, number>
When the key schema is a z.enum(), Zod 4 exhaustively checks all enum members exist as
keys. For partial records, use z.partialRecord(). For pass-through of non-matching keys,
use z.looseRecord().
Arrays and tuples
z.array(z.string()).min(1).max(10);
// nonempty — ⚠ Zod 4 infers string[], not [string, ...string[]]
z.array(z.string()).nonempty();
// For the old tuple-style nonempty, use z.tuple with rest:
z.tuple([z.string()], z.string()); // [string, ...string[]]
File validation
z.file().min(10_000).max(1_000_000).mime(["image/png", "image/jpeg"]);
§2 Parsing and safe parsing
const result = User.safeParse(untrustedInput);
if (result.success) {
result.data; // fully typed User
} else {
result.error; // ZodError
}
// Throwing variant
const user = User.parse(untrustedInput);
Async variants (parseAsync, safeParseAsync) exist for schemas with async refinements
or transforms.
§3 Error handling
Pretty printing
const err = User.safeParse(bad).error!;
console.log(z.prettifyError(err));
// ✖ Invalid input: expected string, received number
// → at name
Structured error trees
.format() and .flatten() are deprecated. Use z.treeifyError():
const tree = z.treeifyError(err);
// tree.name?.errors → string[]
// tree.age?.errors → string[]
Customizing error messages
A single unified error parameter replaces message, invalid_type_error,
required_error, and errorMap:
z.string().min(5, { error: "Too short" });
// Function form — replaces errorMap, invalid_type_error, required_error
z.string({
error: (issue) =>
issue.input === undefined ? "Required" : "Expected a string",
});
// Returning undefined yields to the next error map in the chain
z.string().min(5, {
error: (issue) => issue.code === "too_small" ? `Need >${issue.minimum} chars` : undefined,
});
Internationalization
z.config(z.locales.en()); // or z.locales.de(), z.locales.ja(), etc.
Issue types
Zod 4 consolidated issue types. The base interface remains stable:
interface $ZodIssueBase {
readonly code?: string;
readonly input?: unknown;
readonly path: PropertyKey[];
readonly message: string;
}
Notable merges: invalid_enum_value/invalid_literal → invalid_value;
invalid_date/not_finite → invalid_type. Infinities always rejected by z.number().
§4 Type inference
type UserInput = z.input<typeof User>; // type before transforms
type UserOutput = z.output<typeof User>; // type after transforms (= z.infer)
type User = z.infer<typeof User>; // alias for z.output
⚠ Zod 3 trap: z.any() and z.unknown() properties in objects are no longer
optional in the inferred type. { a: z.any() } infers { a: any }, not { a?: any }.
§5 Migration traps (Zod 3 → 4)
This section is a concise checklist. For the full migration guide, see
https://zod.dev/v4/changelog. A community codemod zod-v3-to-v4 is available.
High impact
message→errorin all refinement / check options.messagestill works but is deprecated.invalid_type_error/required_error— dropped entirely. Useerrorfunction form.errorMap— renamed toerror. Can now return a plain string or undefined..format()/.flatten()— deprecated. Usez.treeifyError()..merge()— deprecated. Use.extend(other.shape)or shape spread.z.nativeEnum()— deprecated.z.enum()handles TS enums directly.z.record(valueSchema)— single-arg form removed. Always pass key + value schemas.z.string().email()etc — deprecated. Usez.email(),z.url(),z.uuidv4()..strict()/.passthrough()— deprecated. Usez.strictObject()/z.looseObject().z.function()— no longer a schema. New factory API withinput/outputparams.
Semantic changes (silent breakage risk)
z.number()rejectsInfinity/-Infinity..int()rejects unsafe integers (outsideNumber.MIN_SAFE_INTEGER..MAX_SAFE_INTEGER)..default()now short-circuits. The default value must match the output type, not the input type. For pre-parse defaults, use.prefault().- Defaults inside optional fields are applied:
z.string().default("x").optional()yields"x"when the key is missing, notundefined. z.unknown()/z.any()object properties are required in the inferred type..nonempty()on arrays infersT[], not[T, ...T[]].- Refinements via type predicates no longer narrow in
.refine()(restored in 4.3 — see §8). - Error map precedence changed: schema-level
errornow takes priority over parse-timeerror. z.record()with enum keys is now exhaustive — all enum members must be present. Usez.partialRecord()for the old behavior..pick()/.omit()on refined schemas now throws (4.3+) — previously silently dropped refinements.
§6 Codecs (4.1+)
Codecs solve the serialization boundary problem. Where .transform() is unidirectional,
a codec defines both directions, enabling .encode() to round-trip data.
const isoDate = z.codec(z.iso.datetime(), z.date(), {
decode: (iso) => new Date(iso),
encode: (date) => date.toISOString(),
});
isoDate.decode("2025-01-15T10:30:00.000Z"); // → Date
isoDate.encode(new Date()); // → "2025-01-15T..."
isoDate.parse("2025-01-15T10:30:00.000Z"); // → Date (same as decode)
Key constraint: .transform() is incompatible with .encode() — calling .encode()
on a pipeline containing .transform() throws. Refactor to codecs when you need
round-tripping.
Codecs compose via .pipe():
const JsonParams = jsonCodec.pipe(z.object({ name: z.string(), age: z.number() }));
JsonParams.decode('{"name":"Alice","age":30}'); // → { name: "Alice", age: 30 }
JsonParams.encode({ name: "Bob", age: 25 }); // → JSON string
Most non-transforming schemas behave identically under .decode() and .encode(). The
behaviour diverges for codecs, .default() (short-circuits on decode, rejects undefined
on encode), .catch() (decode only), and z.stringbool() (a codec internally).
For a full library of codec implementations (JSON, hex, base64, URL, epoch, etc.), see
references/codec-recipes.md.
§7 Metadata, registries, and JSON Schema
Registries
Store strongly typed metadata outside the schema itself, in a registry:
const apiRegistry = z.registry<{
title: string;
description: string;
deprecated?: boolean;
}>();
const Email = z.email();
apiRegistry.add(Email, { title: "Email", description: "User email address" });
apiRegistry.get(Email); // → { title: "Email", ... }
Global registry and .meta()
z.string().meta({
id: "user_name",
title: "Username",
description: "Unique handle",
examples: ["alice42"],
});
// Shorthand for description only (.describe() still works but .meta() is preferred)
z.string().meta({ description: "A username" });
JSON Schema conversion
// Zod → JSON Schema
const jsonSchema = z.toJSONSchema(User);
// JSON Schema → Zod (experimental, 4.3+)
const zodSchema = z.fromJSONSchema({
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
});
z.toJSONSchema() pulls metadata from z.globalRegistry automatically. For full options
(including $refStrategy, effectStrategy, named definitions), see
references/json-schema-options.md.
z.fromJSONSchema() supports JSON Schema draft-2020-12, draft-7, draft-4, and OpenAPI 3.0.
Consider it experimental — no guarantee of round-trip soundness through
toJSONSchema → fromJSONSchema.
§8 Advanced patterns
Recursive types (native — no casts needed)
Use getter syntax. Unlike Zod 3's z.lazy() pattern, no type assertion required, and the
result is a full ZodObject with .pick(), .partial(), etc.
const Category = z.object({
name: z.string(),
get subcategories() { return z.array(Category); },
});
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }
Mutual recursion works identically via cross-referencing getters.
Template literal types
Represent TypeScript template literal types with validated parsing. String format schemas
(e.g. z.email()) work inside — their internal regexes are concatenated. Custom
refinements are not enforced.
const CssValue = z.templateLiteral([z.number(), z.enum(["px", "em", "rem", "%"])]);
CssValue.parse("16px"); // ✅
Discriminated unions (upgraded)
Now support union/pipe discriminators and compose — a discriminated union can be a member
of another. See the z.discriminatedUnion() docs for examples.
z.xor() (4.3+)
Exclusive union — requires exactly one match. Produces oneOf in JSON Schema.
const schema = z.xor([
z.object({ type: z.literal("user"), name: z.string() }),
z.object({ type: z.literal("admin"), role: z.string() }),
]);
Type predicate refinements (restored in 4.3)
const isString = z.unknown().refine((v): v is string => typeof v === "string");
type T = z.output<typeof isString>; // string
Branded types (enhanced in 4.3)
z.string().brand<"UserId">(); // output only (default)
z.string().brand<"UserId", "in">(); // input only
z.string().brand<"UserId", "inout">(); // both
.apply() (4.3+) — factor out reusable check pipelines
const clamp = <T extends z.ZodNumber>(s: T) => s.min(0).max(100);
z.number().apply(clamp).nullable();
z.stringbool() — env var booleans
Truthy: "true", "1", "yes", "on", "y", "enabled". Falsy: "false", "0", "no", "off", "n",
"disabled". Customizable via { truthy: [...], falsy: [...] }.
Zod Mini and library authoring
Zod Mini (zod/mini): functional, tree-shakable API (~1.88 KB gzipped). See
references/zod-mini-mapping.md for the full method→function mapping.
For library authors: depend on zod/v4/core for compatibility with both Classic and Mini.
It exports $ZodType, $ZodCheck, $ZodError and top-level parsing functions. The
"zod/v4/core" subpath is a permanent, stable permalink.
Best practices
- Top-level format constructors (
z.email(), notz.string().email()). z.strictObject()at API boundaries to catch unexpected fields.- Codecs for serialization boundaries — API responses, form data, env vars.
.meta()and registries to drive JSON Schema / OpenAPI generation.- Shape spread over
.extend()for best tsc performance in large schemas. .safeParse()over.parse()in user-facing paths — avoid try/catch for expected validation failures.z.prettifyError()for humans;z.treeifyError()for programmatic access.z.int()/z.int32()overz.number().int()for clarity and built-in ranges.z.xor()when exactly one variant must match — correctoneOfin JSON Schema.z.stringbool()for env vars instead of hand-rolled coercion.
Forward compatibility
- Method-form string formats (
.email(),.uuid()) will be removed in Zod 5. .strict(),.passthrough(),.merge(),.describe()— deprecated but retained.z.fromJSONSchema()is experimental and may change in minor releases.zod/v4/coreis the stable contract for library authors — prefer it over internals.