# Zod User Guide

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 you `import { z } from "zod"`. Legacy Zod 3 remains available from the same package at `zod/v3`, and a tree-shakable variant called **Zod Mini** is available at `zod/mini`. See [Zod Mini](#9-zod-mini) and [Migrating between versions](#version-map) below.

---

## Table of Contents

1. [Quick Start](#1-quick-start)
2. [Installation](#2-installation)
3. [Core Concepts](#3-core-concepts)
4. [Defining Schemas](#4-defining-schemas)
5. [Parsing and Validation](#5-parsing-and-validation)
6. [Customizing and Refining](#6-customizing-and-refining)
7. [Transforms, Defaults, and Codecs](#7-transforms-defaults-and-codecs)
8. [Handling Errors](#8-handling-errors)
9. [Zod Mini](#9-zod-mini)
10. [Metadata, JSON Schema, and i18n](#10-metadata-json-schema-and-i18n)
11. [Common Tasks](#11-common-tasks)
12. [Troubleshooting](#12-troubleshooting)
13. [FAQ](#13-faq)
14. [API Reference](#14-api-reference)

---

## 1. Quick Start

Install Zod, define a schema, and parse some data:

```bash
npm install zod
```

```ts
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"

    ```bash
    npm install zod
    ```

=== "pnpm"

    ```bash
    pnpm add zod
    ```

=== "yarn"

    ```bash
    yarn add zod
    ```

=== "bun"

    ```bash
    bun add zod
    ```

=== "Deno (JSR)"

    ```bash
    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 `strict` mode in your `tsconfig.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.

```json
{
  "compilerOptions": {
    "strict": true
  }
}
```

### Import styles

All of these are valid and equivalent for the main API:

```ts
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](#9-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:

```ts
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](#7-transforms-defaults-and-codecs)).

### 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:

```ts
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):

```ts
type In = z.input<typeof Point>;
type Out = z.output<typeof Point>; // identical to z.infer
```

> [!NOTE]
> `z.infer` is an alias for `z.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

```ts
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). |

```ts
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:**

```ts
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 |
|---|---|---|
| Email | `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). Prefer `z.cuid2()` for new code.

**ISO date/time formats** live under the `z.iso` namespace:

```ts
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

```ts
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

```ts
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:

```ts
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](#6-customizing-and-refining)), use `.safeExtend()` instead — the source rejects key-manipulation methods on refined object schemas to prevent silently dropping your validation logic.

### Arrays, tuples, and collections

```ts
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

```ts
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:

```ts
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.discriminatedUnion` whenever your union members are objects with a common literal "tag" field (like `type` or `kind`). 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

```ts
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:

```ts
const Category = z.object({
  name: z.string(),
  subcategories: z.lazy(() => z.array(Category)),
});
```

### Optional, nullable, and nullish

```ts
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:

```ts
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)
```

```ts
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 to `true`. Only `""`, `0`, `null`, and `undefined` coerce to `false`. 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

```ts
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`](#8-handling-errors)).

### `.safeParse()` — return a result object

```ts
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**:

```ts
{ 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:

```ts
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 `async` refinement 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](#9-zod-mini) but works everywhere:

```ts
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

```ts
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:

```ts
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()`:

```ts
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 a `z.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:

```ts
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

```ts
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:

```ts
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`

```ts
z.string().default("anonymous").parse(undefined); // → "anonymous"
z.string().default(() => generateId()).parse(undefined); // lazy default
```

### `.catch()` — recover from any failure

```ts
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 is `undefined`; `.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:

```ts
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

```ts
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:

```ts
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 throws `Encountered 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:

```ts
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:

```ts
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:

```text
✖ Invalid input: expected string, received number
  → at name
```

`z.flattenError` is ideal for form libraries — it separates top-level errors from per-field errors:

```ts
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.treeifyError` preserves 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).

```ts
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 `zod` unless 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()`:

```ts
const Name = z.string().describe("The user's full name");
```

For richer metadata, register schemas with the **global registry** or your own registry:

```ts
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:

```ts
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`:

```ts
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

```ts
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

```ts
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.env` values are always strings, and coercion converts `"3000"` into the number `3000` for you. Pair it with `.default()` to supply sensible fallbacks.

### Model a recursive (tree) structure

```ts
const TreeNode = z.object({
  value: z.string(),
  children: z.lazy(() => z.array(TreeNode)),
});
```

### Reuse and derive schemas

```ts
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

```ts
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:

```ts
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](#6-customizing-and-refining).

**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.*`

---

_Generated June 2026 from colinhacks/zod at bbc68f9. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
