| 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 via zod/v4/core
For codec recipes, JSON Schema options, and zod/mini API mappings, see the
references/ 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).
z.email()
z.uuidv4()
z.url()
z.ipv4()
z.iso.datetime()
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]);
Numeric formats
Fixed-width types with built-in range constraints:
z.int()
z.int32()
z.uint32()
z.float32()
z.float64()
z.int64()
z.uint64()
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() });
z.looseObject({ name: z.string() });
z.object({ name: z.string() });
Extending and composing objects
Use .extend() or shape spread. .merge() is deprecated.
const WithName = Base.extend({ name: z.string() });
const WithName2 = z.object({ ...Base.shape, name: z.string() });
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"]);
enum Direction { Up = "UP", Down = "DOWN" }
const Dir = z.enum(Direction);
Access values via .enum (.Enum and .Values are removed):
Status.enum.active;
Records
z.record() now requires two arguments (key schema, value schema). The single-argument
form is removed.
z.record(z.string(), z.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);
z.array(z.string()).nonempty();
z.tuple([z.string()], z.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;
} else {
result.error;
}
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));
Structured error trees
.format() and .flatten() are deprecated. Use z.treeifyError():
const tree = z.treeifyError(err);
Customizing error messages
A single unified error parameter replaces message, invalid_type_error,
required_error, and errorMap:
z.string().min(5, { error: "Too short" });
z.string({
error: (issue) =>
issue.input === undefined ? "Required" : "Expected a string",
});
z.string().min(5, {
error: (issue) => issue.code === "too_small" ? `Need >${issue.minimum} chars` : undefined,
});
Internationalization
z.config(z.locales.en());
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 UserOutput = z.output<typeof User>;
type User = z.infer<typeof User>;
⚠ 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 → error in all refinement / check options. message still works but is
deprecated.
invalid_type_error / required_error — dropped entirely. Use error function
form.
errorMap — renamed to error. Can now return a plain string or undefined.
.format() / .flatten() — deprecated. Use z.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. Use z.email(), z.url(), z.uuidv4().
.strict() / .passthrough() — deprecated. Use z.strictObject() /
z.looseObject().
z.function() — no longer a schema. New factory API with input/output params.
Semantic changes (silent breakage risk)
z.number() rejects Infinity/-Infinity.
.int() rejects unsafe integers (outside Number.MIN_SAFE_INTEGER to
Number.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, not undefined.
z.unknown() / z.any() object properties are required in the inferred type.
.nonempty() on arrays infers T[], not [T, ...T[]].
- Refinements via type predicates no longer narrow in
.refine() (restored in 4.3
— see §8).
- Error map precedence changed: schema-level
error now takes priority over
parse-time error.
z.record() with enum keys is now exhaustive — all enum members must be present.
Use z.partialRecord() for the old behaviour.
.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");
isoDate.encode(new Date());
isoDate.parse("2025-01-15T10:30:00.000Z");
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}');
JsonParams.encode({ name: "Bob", age: 25 });
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);
Global registry and .meta()
z.string().meta({
id: "user_name",
title: "Username",
description: "Unique handle",
examples: ["alice42"],
});
z.string().meta({ description: "A username" });
JSON Schema conversion
const jsonSchema = z.toJSONSchema(User);
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>;
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>;
Branded types (enhanced in 4.3)
z.string().brand<"UserId">();
z.string().brand<"UserId", "in">();
z.string().brand<"UserId", "inout">();
.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(), not z.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() over z.number().int() for clarity and built-in ranges.
z.xor() when exactly one variant must match — correct oneOf in 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/core is the stable contract for library authors — prefer it over internals.