Zod is a TypeScript-first schema declaration and validation library. You define a schema once — a description of the shape your data should have — and Zod gives you back two things from that single definition: a runtime validator that checks unknown data, and a static TypeScript type that you never have to write or maintain by hand.
This guide shows you how to install Zod, define schemas for every kind of data, parse and validate input, handle errors, transform values, and infer types. Everything here is derived directly from the Zod source code at the documented commit.
Note
This guide documents Zod 4 (package version
4.4.3), the current major version. Zod 4 is the default when youimport { z } from "zod". Legacy Zod 3 remains available from the same package atzod/v3, and a tree-shakable variant called Zod Mini is available atzod/mini. See Zod Mini and Migrating between versions below.
Table of Contents
- Quick Start
- Installation
- Core Concepts
- Defining Schemas
- Parsing and Validation
- Customizing and Refining
- Transforms, Defaults, and Codecs
- Handling Errors
- Zod Mini
- Metadata, JSON Schema, and i18n
- Common Tasks
- Troubleshooting
- FAQ
- API Reference
1. Quick Start
Install Zod, define a schema, and parse some data:
npm install zod
import { z } from "zod";
// 1. Define a schema
const User = z.object({
name: z.string(),
age: z.number().int().positive(),
email: z.string().email(),
});
// 2. Infer the static type (no manual interface needed)
type User = z.infer<typeof User>;
// { name: string; age: number; email: string }
// 3. Parse unknown data at runtime
const user = User.parse({
name: "Alice",
age: 30,
email: "[email removed]",
});
// `user` is fully typed as User
// 4. Or validate without throwing
const result = User.safeParse({ name: "Bob", age: -1, email: "nope" });
if (!result.success) {
console.log(result.error.issues);
} else {
console.log(result.data);
}
That is the entire workflow: declare once, get a type and a validator. Every section below expands on a piece of this.
Tip
The
z.infer<typeof Schema>trick is the heart of Zod. Define your validation logic once and let TypeScript derive the type — your runtime checks and your compile-time types can never drift apart.
2. Installation
Zod ships as a single dependency-free package on npm. Install it with your package manager of choice.
npm
npm install zod
pnpm
pnpm add zod
yarn
yarn add zod
bun
bun add zod
Deno (JSR)
deno add jsr:@zod/zod
Requirements
| Requirement | Detail |
|---|---|
| TypeScript | 5.5 or later recommended (the repo builds and tests against ~5.5.4). |
strict mode |
Required. Zod's type inference depends on "strict": true in your tsconfig.json. |
| Runtime | Any modern JavaScript runtime — Node.js, Deno, Bun, browsers, edge/serverless. Zod has zero runtime dependencies. |
| Module system | Ships both ESM and CommonJS builds; works with import and require. |
Important
Enable
strictmode in yourtsconfig.json. Without it, Zod's inferred types silently degrade (for example,.optional()fields will not be correctly typed). This is the single most common setup mistake.
{
"compilerOptions": {
"strict": true
}
}
Import styles
All of these are valid and equivalent for the main API:
import { z } from "zod"; // named import (most common)
import * as z from "zod"; // namespace import
import z from "zod"; // default import
The package also exposes several subpath entry points for specific use cases:
| Import path | What you get |
|---|---|
zod |
Zod 4 classic API (the default). |
zod/mini |
Zod Mini — a tree-shakable, function-based API. |
zod/v3 |
The legacy Zod 3 API, unchanged, for incremental migration. |
zod/v4 |
Zod 4 classic API, explicitly (identical to zod). |
zod/v4/mini |
Zod 4 Mini, explicitly. |
zod/v4/core |
The low-level core engine (for library authors — see the Admin Guide). |
zod/v4/locales |
Built-in error-message translations. |
3. Core Concepts
Three ideas underpin everything in Zod.
Schemas
A schema is an object that describes the expected shape of some data. You build schemas by calling factory functions on the z namespace and chaining methods:
const schema = z.string().min(3).max(20);
Every schema knows how to validate data against itself and carries two associated types: its input type and its output type (usually the same, but they differ when you use transforms — see Section 7).
Parsing
You apply a schema to data by parsing it. Parsing checks the data and either returns a typed, validated value or reports what went wrong. Zod gives you throwing (.parse) and non-throwing (.safeParse) variants, plus async forms for schemas that contain async logic.
Type inference
From any schema you can extract a static TypeScript type with the z.infer helper:
const Point = z.object({ x: z.number(), y: z.number() });
type Point = z.infer<typeof Point>; // { x: number; y: number }
You can also pull out the input and output types separately with z.input and z.output (these matter when a schema transforms its data):
type In = z.input<typeof Point>;
type Out = z.output<typeof Point>; // identical to z.infer
Note
z.inferis an alias forz.output. For schemas without transforms, input and output are the same type, so you rarely need to distinguish them.
4. Defining Schemas
This section catalogs every schema type Zod provides. Each is a function on the z namespace.
Primitives
z.string();
z.number();
z.boolean();
z.bigint();
z.symbol();
z.date();
z.undefined();
z.null();
z.void();
z.any();
z.unknown();
z.never();
z.nan();
z.file();
| Schema | Accepts |
|---|---|
z.string() |
A string. |
z.number() |
A finite number (rejects NaN). |
z.boolean() |
A boolean. |
z.bigint() |
A bigint. |
z.symbol() |
A symbol. |
z.date() |
A Date instance. |
z.null() |
The value null. |
z.undefined() |
The value undefined. |
z.void() |
undefined (semantic alias for "no return value"). |
z.any() / z.unknown() |
Any value (the difference is the inferred TypeScript type). |
z.never() |
Nothing — always fails. |
z.nan() |
The value NaN. |
z.file() |
A File instance (browser / Web file uploads). |
const name = z.string();
name.parse("hello"); // → "hello"
name.parse(123); // throws ZodError
String validations and formats
z.string() supports a rich set of chained checks and built-in format validators.
Length and content checks:
z.string().min(3); // minimum length
z.string().max(20); // maximum length
z.string().length(10); // exact length
z.string().regex(/^[a-z]+$/); // matches a pattern
z.string().includes("foo"); // contains a substring
z.string().startsWith("https://");
z.string().endsWith(".com");
z.string().lowercase(); // must be all lowercase
z.string().uppercase(); // must be all uppercase
Built-in string formats — each is available both as a standalone schema (z.email()) and, in many cases, as a method:
| Format | Schema | Validates |
|---|---|---|
z.email() |
An email address. | |
| URL | z.url() |
A URL. |
| UUID | z.uuid() |
Any UUID. Version-specific: z.uuidv4(), z.uuidv6(), z.uuidv7(). |
| GUID | z.guid() |
A GUID. |
| Emoji | z.emoji() |
A string of emoji. |
| Nano ID | z.nanoid() |
A Nano ID. |
| CUID / CUID2 | z.cuid(), z.cuid2() |
Collision-resistant IDs. |
| ULID | z.ulid() |
A ULID. |
| XID / KSUID | z.xid(), z.ksuid() |
Other sortable ID formats. |
| IPv4 / IPv6 | z.ipv4(), z.ipv6() |
IP addresses. |
| MAC | z.mac() |
A MAC address. |
| CIDR | z.cidrv4(), z.cidrv6() |
CIDR network ranges. |
| Base64 | z.base64(), z.base64url() |
Base64-encoded data. |
| E.164 | z.e164() |
An international phone number. |
| JWT | z.jwt() |
A JSON Web Token. |
Warning
z.cuid()validates CUID v1, which its own authors have deprecated because timestamps are embedded in the id (an information-leak risk). Preferz.cuid2()for new code.
ISO date/time formats live under the z.iso namespace:
z.iso.datetime(); // 2021-01-01T00:00:00Z
z.iso.datetime({ offset: true }); // allows timezone offsets (+07:00)
z.iso.datetime({ local: true }); // allows local times with no Z
z.iso.datetime({ precision: 3 }); // requires millisecond precision
z.iso.date(); // 2021-01-01
z.iso.time(); // 00:00:00
z.iso.time({ precision: 3 }); // 00:00:00.123
z.iso.duration(); // P3Y6M4DT12H30M5S
The following table shows exactly what z.iso.datetime() accepts under different options (verified against the test suite):
| Input | default | {local: true} |
{offset: true} |
{precision: 3} |
|---|---|---|---|---|
2021-01-01T00:00:00Z |
✅ | ✅ | ✅ | ❌ |
2021-01-01T00:00:00.123Z |
✅ | ✅ | ✅ | ✅ |
2021-01-01T00:00:00 (no zone) |
❌ | ✅ | ❌ | ❌ |
2021-01-01T00:00:00+07:00 |
❌ | ❌ | ✅ | ❌ |
Number validations and formats
z.number().min(0); // alias: .gte(0)
z.number().max(100); // alias: .lte(100)
z.number().gt(0); // strictly greater than
z.number().lt(100); // strictly less than
z.number().int(); // must be a safe integer
z.number().positive(); // > 0
z.number().negative(); // < 0
z.number().nonnegative(); // >= 0
z.number().nonpositive(); // <= 0
z.number().multipleOf(5); // divisible by 5
Fixed-width numeric formats are also available as top-level schemas:
| Schema | Range / meaning |
|---|---|
z.int() |
A safe integer (Number.isSafeInteger). |
z.int32() / z.uint32() |
32-bit signed / unsigned integer. |
z.float32() / z.float64() |
32-/64-bit float. |
z.int64() / z.uint64() |
64-bit bigint integers. |
Objects
const User = z.object({
name: z.string(),
age: z.number(),
email: z.string().email().optional(),
});
By default, unrecognized keys are stripped from the output. You can change that behavior:
| Method / schema | Unknown keys |
|---|---|
z.object({...}) |
Stripped (default). |
z.strictObject({...}) |
Rejected — parsing fails. |
z.looseObject({...}) |
Passed through unchanged. |
.catchall(schema) |
Validated against schema instead of stripped. |
Object schemas have a powerful set of methods for deriving new schemas:
User.extend({ role: z.string() }); // add fields
User.pick({ name: true }); // keep only some fields
User.omit({ email: true }); // drop some fields
User.partial(); // make all fields optional
User.required(); // make all fields required
User.keyof(); // a z.enum of the keys
User.shape; // access the field schemas
Note
.extend()and.partial()operate on a plain object schema. If you have added refinements to the object (see Section 6), use.safeExtend()instead — the source rejects key-manipulation methods on refined object schemas to prevent silently dropping your validation logic.
Arrays, tuples, and collections
z.array(z.string()); // string[]
z.string().array(); // equivalent, chained form
z.array(z.number()).min(1).max(10); // length-constrained
z.tuple([z.string(), z.number()]); // [string, number]
z.tuple([z.string()], z.boolean()); // [string, ...boolean[]] (rest element)
z.set(z.string()); // Set<string>
z.map(z.string(), z.number()); // Map<string, number>
z.record(z.string(), z.number()); // Record<string, number>
Unions, intersections, and enums
z.union([z.string(), z.number()]); // string | number
z.string().or(z.number()); // equivalent, chained form
z.intersection(schemaA, schemaB); // A & B
schemaA.and(schemaB); // equivalent, chained form
z.enum(["red", "green", "blue"]); // "red" | "green" | "blue"
z.enum(MyTypeScriptEnum); // from a native TS enum
z.literal("hello"); // the exact value "hello"
z.literal(["a", "b"]); // "a" | "b"
For unions of objects that share a discriminator field, use z.discriminatedUnion — it is faster and produces clearer errors:
const Shape = z.discriminatedUnion("type", [
z.object({ type: z.literal("circle"), radius: z.number() }),
z.object({ type: z.literal("square"), side: z.number() }),
]);
Tip
Reach for
z.discriminatedUnionwhenever your union members are objects with a common literal "tag" field (liketypeorkind). Zod uses the discriminator to jump straight to the right member instead of trying each in turn, and the error message points at the actual mismatch rather than dumping every branch's failures.
Special and composite schemas
z.templateLiteral([z.string(), z.number()]); // `${string}${number}`
z.lazy(() => z.string()); // for recursive schemas
z.promise(z.string()); // Promise<string>
z.json(); // any JSON-serializable value
z.instanceof(MyClass); // an instance of a class
z.custom<`${number}px`>((val) => /^\d+px$/.test(val as string));
z.lazy is how you model recursive types such as trees or linked lists:
const Category = z.object({
name: z.string(),
subcategories: z.lazy(() => z.array(Category)),
});
Optional, nullable, and nullish
z.string().optional(); // string | undefined
z.string().nullable(); // string | null
z.string().nullish(); // string | null | undefined
z.optional(z.string()); // standalone forms also exist
z.nullable(z.string());
Use .nonoptional() to remove a previously applied .optional().
Coercion
The z.coerce namespace builds schemas that convert their input before validating, using the corresponding JavaScript constructor:
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)
z.coerce.number().parse("42"); // → 42 (string coerced to number)
z.coerce.date().parse("2021-01-01T00:00:00Z"); // → Date
Caution
Coercion uses JavaScript's built-in conversion rules, which can surprise you.
z.coerce.boolean()follows JS truthiness, so any non-empty string — including"false"— coerces totrue. Only"",0,null, andundefinedcoerce tofalse. If you need to parse the literal strings"true"/"false", write a custom transform instead.
5. Parsing and Validation
Once you have a schema, there are four ways to run it against data.
.parse() — throw on failure
const value = z.string().parse("hello"); // → "hello"
z.string().parse(123); // throws ZodError
Use .parse() when invalid data is a programming error you want to surface immediately (it throws a ZodError).
.safeParse() — return a result object
const result = z.string().safeParse(123);
if (result.success) {
result.data; // the typed, validated value
} else {
result.error; // a ZodError
}
.safeParse() never throws. It returns a discriminated union:
{ success: true; data: T }
{ success: false; error: ZodError }
This is the right choice for validating external input (form submissions, API request bodies, environment variables) where failure is expected and you want to handle it gracefully.
Async parsing
If a schema contains an async refinement or transform, you must parse it asynchronously, otherwise Zod throws a synchronous-parse error:
const schema = z.string().refine(async (s) => await isUnique(s));
await schema.parseAsync("abc"); // throws ZodError on failure
await schema.safeParseAsync("abc"); // returns the result object
| Method | Throws? | Returns | Use when |
|---|---|---|---|
.parse(data) |
Yes | T |
Failure is a bug; you want an exception. |
.safeParse(data) |
No | { success, data | error } |
Failure is expected; handle it inline. |
.parseAsync(data) |
Yes | Promise<T> |
Schema has async logic. |
.safeParseAsync(data) |
No | Promise<{ success, ... }> |
Async logic, no throwing. |
Important
A schema that contains any
asyncrefinement or transform cannot be run with the synchronous.parse()/.safeParse(). Attempting it throws:Encountered Promise during synchronous parse. Use .parseAsync() instead.Switch to the async variants.
Functional parsing
Every parse method is also available as a top-level function that takes the schema as its first argument. This form is the primary API in Zod Mini but works everywhere:
z.parse(schema, data);
z.safeParse(schema, data);
z.parseAsync(schema, data);
z.safeParseAsync(schema, data);
6. Customizing and Refining
When the built-in validations are not enough, add your own logic with refinements.
.refine() — custom predicate
const Password = z
.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), {
message: "Must contain an uppercase letter",
});
A refinement is a function that returns truthy for valid values. If it returns falsy, Zod records an issue. You can customize the message and the error path:
const Form = z
.object({ password: z.string(), confirm: z.string() })
.refine((data) => data.password === data.confirm, {
message: "Passwords do not match",
path: ["confirm"], // attach the error to the `confirm` field
});
.superRefine() and .check() — multiple issues
For full control — emitting several issues, or inspecting the raw context — use .check() (Zod 4) with z.check, or the Zod 3-style .superRefine():
const schema = z.array(z.string()).check((ctx) => {
if (ctx.value.length > 3) {
ctx.issues.push({
code: "custom",
message: "Too many items",
input: ctx.value,
});
}
});
Note
.with()is an alias for.check()and behaves identically — both attach a check to a schema. You can pass either az.refine(...)/z.check(...)object or a plain context-taking function.
Custom error messages
Most schemas and checks accept an error message as a string argument or via an error option:
z.string("Name is required");
z.string().min(3, "Too short");
z.string().min(3, { error: "Too short" });
7. Transforms, Defaults, and Codecs
Schemas can change their data as it passes through, not just validate it.
.transform() — reshape the output
const schema = z.string().transform((val) => val.length);
schema.parse("hello"); // → 5 (a number!)
type In = z.input<typeof schema>; // string
type Out = z.output<typeof schema>; // number
This is where input and output types diverge: the schema accepts a string but produces a number.
.pipe() — chain schemas
.pipe() feeds the output of one schema into another, letting you validate, transform, then validate again:
const schema = z
.string()
.transform((val) => val.length)
.pipe(z.number().min(1));
schema.parse("hello"); // → 5
schema.parse(""); // throws — length 0 fails the .min(1)
.default() — supply a fallback for undefined
z.string().default("anonymous").parse(undefined); // → "anonymous"
z.string().default(() => generateId()).parse(undefined); // lazy default
.catch() — recover from any failure
z.number().catch(0).parse("not a number"); // → 0
// The catch callback receives the error context:
z.number().catch((ctx) => ctx.error.issues.length).parse("oops"); // → 1
Tip
The difference between
.default()and.catch():.default()only fires when the input isundefined;.catch()fires on any validation failure. Use.default()for "missing means X", and.catch()for "if anything is wrong, fall back to X".
.brand() — nominal typing
Branding makes two structurally identical types incompatible at the type level — useful for things like validated IDs:
const UserId = z.string().brand<"UserId">();
type UserId = z.infer<typeof UserId>; // string & { brand }
function load(id: UserId) {/* ... */}
load("raw string"); // ❌ compile error — must go through the schema first
.readonly() — freeze the output
const schema = z.object({ x: z.number() }).readonly();
// output type is Readonly<{ x: number }>, and Object.freeze is applied
Codecs — two-way transforms
Zod 4 introduces codecs: schemas that can run in both directions. Alongside the usual decode path (.parse, which goes input → output), you can encode to go output → input:
z.decode(schema, input); // input → output (same as parse)
z.encode(schema, output); // output → input (reverse)
z.decodeAsync(schema, input);
z.encodeAsync(schema, output);
z.safeDecode(schema, input);
z.safeEncode(schema, output);
Note
Encoding only works through bidirectional transforms. A one-way
.transform()cannot be reversed; attempting to encode through one throwsEncountered unidirectional transform during encode.
8. Handling Errors
When validation fails, Zod produces a ZodError.
The ZodError object
A ZodError carries an issues array. Each issue describes one validation failure:
const result = z.object({ name: z.string() }).safeParse({ name: 123 });
if (!result.success) {
console.log(result.error.issues);
// [
// {
// code: "invalid_type",
// expected: "string",
// path: ["name"],
// message: "Invalid input: expected string, received number"
// }
// ]
}
Each issue has at least code, path (the location of the failure within the data), and message. The full set of issue codes:
| Code | Meaning |
|---|---|
invalid_type |
Value is the wrong type (e.g. number where string expected). |
too_big |
Exceeds a maximum length/size/value. |
too_small |
Below a minimum length/size/value. |
invalid_format |
A string-format check failed (email, url, regex, etc.). |
not_multiple_of |
Number is not a multiple of the required divisor. |
unrecognized_keys |
A strict object received unknown keys. |
invalid_union |
No union member matched (or multiple matched ambiguously). |
invalid_key |
A map/record key failed validation. |
invalid_element |
A map/set element failed validation. |
invalid_value |
Value is not one of the allowed literals/enum values. |
custom |
A custom .refine() / .check() failed. |
Formatting errors for display
Zod ships several helpers that reshape a ZodError into formats convenient for different surfaces:
import { z } from "zod";
const err = result.error;
z.prettifyError(err); // a multi-line human-readable string
z.treeifyError(err); // a nested tree mirroring your data shape
z.flattenError(err); // { formErrors, fieldErrors } — great for forms
z.formatError(err); // a nested object with _errors arrays
z.prettifyError produces output like:
✖ Invalid input: expected string, received number
→ at name
z.flattenError is ideal for form libraries — it separates top-level errors from per-field errors:
z.flattenError(err);
// {
// formErrors: [],
// fieldErrors: { name: ["Invalid input: expected string"] }
// }
Tip
For one-line-per-field form display, reach for
z.flattenError. For arbitrarily nested data (objects within arrays within objects),z.treeifyErrorpreserves the full structure so you can show an error next to each individual field.
9. Zod Mini
zod/mini is an alternative API surface backed by the same validation engine as classic Zod. It trades method chaining for standalone functions, which lets bundlers tree-shake away the parts of Zod you do not use — meaningfully reducing bundle size for size-sensitive apps (edge functions, browser bundles).
import * as z from "zod/mini";
const schema = z.object({
name: z.string(),
age: z.optional(z.number()),
});
// Validations are composed with z.* functions instead of chained methods:
const constrained = z.string().check(z.minLength(3), z.maxLength(20));
z.parse(schema, { name: "Alice" });
The mental mapping is: where classic Zod writes z.string().min(3).max(20), Zod Mini writes z.string().check(z.minLength(3), z.maxLength(20)). The schema types (z.string, z.object, etc.) are the same; the checks become composable function arguments.
Note
Use classic
zodunless bundle size is a measured concern. The chained API is more ergonomic for most projects. Zod Mini exists specifically for cases where shaving kilobytes matters; both produce identical validation behavior because they share the core engine.
10. Metadata, JSON Schema, and i18n
Attaching metadata
You can annotate schemas with metadata — descriptions, titles, ids — without affecting validation. The simplest method is .describe():
const Name = z.string().describe("The user's full name");
For richer metadata, register schemas with the global registry or your own registry:
const Email = z.string().email().meta({
id: "email",
title: "Email address",
description: "A valid email",
deprecated: false,
});
// Or use a standalone registry:
const myRegistry = z.registry();
myRegistry.add(Email, { id: "email" });
Metadata fields (id, title, description, deprecated, plus any custom keys) are primarily consumed when generating JSON Schema. Metadata is inherited from parent schemas, except for id, which is never inherited.
Generating JSON Schema
Convert any schema to a JSON Schema document:
const schema = z.object({ name: z.string(), age: z.number() });
const jsonSchema = z.toJSONSchema(schema);
Registered metadata flows into the output (titles, descriptions, ids become $defs references).
Warning
Not every Zod type has a JSON Schema equivalent. Converting a schema that contains a
bigint,symbol,Date,Map,Set, function, transform, or custom type throws a descriptive error (for example,Date cannot be represented in JSON Schema). Keep schemas you intend to export to JSON Schema within representable types, or strip the offending parts first.
Internationalized error messages
Zod includes error-message translations for over 45 languages under zod/v4/locales. Activate one globally with z.config:
import { z } from "zod";
import fr from "zod/v4/locales/fr.js";
z.config(fr()); // all error messages now in French
After this, a type error on a French-configured schema reads, for example, Entrée invalide : chaîne attendu, nombre reçu instead of the English default.
Available locales include ar, de, en, es, fr, it, ja, ko, pt, ru, tr, uk, zh-CN, zh-TW, and many more (50+ in total).
11. Common Tasks
Practical, copy-paste recipes for things you will do often.
Validate an API request body
const CreateUser = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(0).optional(),
});
app.post("/users", (req, res) => {
const result = CreateUser.safeParse(req.body);
if (!result.success) {
return res.status(400).json(z.flattenError(result.error));
}
createUser(result.data); // fully typed
});
Parse and validate environment variables
const Env = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
});
export const env = Env.parse(process.env);
Tip
Use
z.coerce.number()for env vars —process.envvalues are always strings, and coercion converts"3000"into the number3000for you. Pair it with.default()to supply sensible fallbacks.
Model a recursive (tree) structure
const TreeNode = z.object({
value: z.string(),
children: z.lazy(() => z.array(TreeNode)),
});
Reuse and derive schemas
const Base = z.object({ id: z.string(), createdAt: z.date() });
const Post = Base.extend({ title: z.string(), body: z.string() });
const PostUpdate = Post.partial(); // every field optional
const PostPreview = Post.pick({ title: true, id: true });
Validate a discriminated message format
const Event = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("key"), key: z.string() }),
]);
const e = Event.parse(incoming); // narrowed by `type` after parsing
12. Troubleshooting
Error: Expected string, received number
Cause: The data you parsed does not match the schema's type — here, a number was supplied where the schema expects a string.
Solution: Inspect error.issues to find the exact path. Either fix the input or, if the conversion is intentional, use z.coerce.string() to convert automatically.
Prevention: Validate at the boundary of your system (API handlers, form submit) with .safeParse, and surface field-level errors to the caller.
Error: Encountered Promise during synchronous parse. Use .parseAsync() instead.
Cause: Your schema contains an async refinement or transform, but you called the synchronous .parse() / .safeParse().
Solution: Switch to .parseAsync() or .safeParseAsync() and await the result.
Prevention: Any schema with async (val) => ... logic anywhere inside it is async end-to-end. Track which schemas are async and always parse them with the async methods.
Error: z.int is not a function (or Cannot read properties of undefined)
Cause: A bundler or shared package pulled in a mismatched or partial copy of Zod — typically when a library bundles Zod 4 and your app also imports Zod, leading to two incompatible instances (reported in community issue #6018).
Solution: Ensure a single version of Zod is installed (deduplicate with your package manager). If a dependency bundles Zod, align its major version with yours.
Prevention: Keep Zod as a single, shared dependency rather than letting libraries bundle their own copy.
Error: Date cannot be represented in JSON Schema
Cause: You called z.toJSONSchema() on a schema containing a type with no JSON Schema equivalent (z.date(), z.bigint(), z.symbol(), z.map(), z.set(), a transform, or a custom type).
Solution: Remove or replace the unrepresentable type before converting. For dates, validate ISO strings with z.iso.datetime() instead of z.date() when JSON Schema export matters.
Prevention: Design schemas intended for JSON Schema export using only JSON-representable types.
Content Security Policy error: unsafe-eval
Cause: Zod 4 compiles validators just-in-time using the Function constructor for speed. A strict CSP that forbids unsafe-eval blocks this (community issue #4461).
Solution: Enable Zod's JIT-less mode, which disables compilation:
z.config({ jitless: true });
Prevention: In CSP-restricted environments (some browser extensions, locked-down web apps), set jitless: true at startup before defining schemas.
.optional() fields are not typed as optional
Cause: TypeScript strict mode is not enabled. Zod's inference relies on it.
Solution: Set "strict": true in your tsconfig.json.
Prevention: Always run Zod under strict mode — it is a hard requirement, not a recommendation.
Validation passes but extra object keys disappear
Cause: z.object() strips unknown keys by default — this is intended behavior, not a bug.
Solution: Use z.looseObject() to pass unknown keys through, z.strictObject() to reject them, or .catchall(schema) to validate them.
Prevention: Choose the object variant that matches your intent up front.
13. FAQ
Q: What is the difference between .parse() and .safeParse()?
.parse() throws a ZodError on failure and returns the value on success. .safeParse() never throws; it returns { success: true, data } or { success: false, error }. Use .safeParse() for untrusted input you expect might be invalid.
Q: How do I get a TypeScript type from a schema?
Use z.infer<typeof Schema>. For schemas with transforms, use z.input<typeof Schema> and z.output<typeof Schema> to get the pre- and post-transform types.
Q: Do I have to install any other dependencies?
No. Zod has zero runtime dependencies. You only need TypeScript (5.5+ recommended) with strict mode for the best inference.
Q: What is Zod Mini and when should I use it?
zod/mini is a function-based, tree-shakable API backed by the same engine. Use it only when bundle size is a measured concern; otherwise the classic chained API is more ergonomic.
Q: Can I still use Zod 3?
Yes. The legacy Zod 3 API is bundled and available at zod/v3 with no changes, so you can migrate incrementally.
Q: How do I make all fields of an object optional?
Call .partial() on the object schema. To make them required again, use .required().
Q: How do I add a custom validation rule?
Use .refine(predicate, message) for a single check, or .check() / .superRefine() when you need to emit multiple issues or inspect the validation context.
Q: How do I validate that two fields match (e.g. password confirmation)?
Apply .refine() to the object schema and use the path option to attach the error to the relevant field. See Section 6.
Q: My union errors are huge and hard to read. What can I do?
If your union members are objects sharing a literal tag field, switch to z.discriminatedUnion(key, [...]). It produces a single targeted error instead of collecting every branch's failures.
Q: How do I change the language of error messages?
Import a locale from zod/v4/locales and pass it to z.config(), e.g. z.config(z.locales.fr()).
Q: How do I turn a schema into JSON Schema?
Call z.toJSONSchema(schema). Note that some Zod types (dates, bigints, maps, sets, transforms) cannot be represented and will throw.
Q: Does coercion handle "false" correctly for booleans?
No — z.coerce.boolean() uses JavaScript truthiness, so "false" becomes true. Use a custom transform if you need string-aware boolean parsing.
Q: How do I provide a default value?
.default(value) supplies a fallback when the input is undefined. Use .catch(value) to fall back on any validation failure.
Q: What is a codec?
A codec is a Zod 4 schema that runs in both directions. z.decode (≡ parse) goes input → output; z.encode reverses it through bidirectional transforms.
14. API Reference
Schema factories
| Category | Functions |
|---|---|
| Primitives | string, number, boolean, bigint, symbol, date, undefined, null, void, any, unknown, never, nan, file |
| String formats | email, url, uuid, uuidv4, uuidv6, uuidv7, guid, emoji, nanoid, cuid, cuid2, ulid, xid, ksuid, ipv4, ipv6, mac, cidrv4, cidrv6, base64, base64url, e164, jwt |
| ISO formats | iso.datetime, iso.date, iso.time, iso.duration |
| Numeric formats | int, int32, uint32, float32, float64, int64, uint64 |
| Coercion | coerce.string, coerce.number, coerce.boolean, coerce.bigint, coerce.date |
| Objects | object, strictObject, looseObject, record |
| Collections | array, tuple, set, map |
| Compositions | union, discriminatedUnion, intersection, enum, nativeEnum, literal, templateLiteral |
| Special | lazy, promise, json, instanceof, custom, optional, nullable, nullish, nonoptional |
| Transforms | transform, preprocess, pipe |
Schema methods (chained)
| Method | Purpose |
|---|---|
.optional() / .nullable() / .nullish() |
Wrap as optional/nullable. |
.default(v) / .catch(v) |
Fallback for undefined / any failure. |
.refine(fn, msg) / .check(fn) / .superRefine(fn) |
Custom validation. |
.transform(fn) / .pipe(schema) |
Reshape / chain output. |
.brand<T>() / .readonly() |
Nominal typing / freeze output. |
.array() / .or(s) / .and(s) |
Build array / union / intersection. |
.min / .max / .length / .regex / etc. |
Type-specific checks. |
.meta(obj) / .describe(text) |
Attach metadata. |
.parse / .safeParse / .parseAsync / .safeParseAsync |
Run validation. |
Object-schema methods
.extend, .safeExtend, .pick, .omit, .partial, .required, .keyof, .catchall, .shape
Parsing functions (top-level)
z.parse, z.safeParse, z.parseAsync, z.safeParseAsync, z.decode, z.encode, z.decodeAsync, z.encodeAsync, z.safeDecode, z.safeEncode
Error utilities
z.prettifyError, z.treeifyError, z.flattenError, z.formatError, z.toDotPath
Type helpers
z.infer, z.input, z.output
Configuration & metadata
z.config({ customError, localeError, jitless }), z.registry(), z.globalRegistry, z.toJSONSchema(schema), z.locales.*