Zod logo

Defining schemas

To validate data, you must first define a schema. Schemas represent types, from simple primitive values to complex nested objects and arrays.

Primitives

import { z } from "zod";
 
// primitive types
z.string();
z.number();
z.bigint();
z.boolean();
z.symbol();
z.undefined();
z.null();

To coerce input data to the appropriate type, use z.coerce instead:

z.coerce.string();    // String(input)
z.coerce.number();    // Number(input)
z.coerce.boolean();   // Boolean(input)
z.coerce.bigint();    // BigInt(input)

The coerced variant of these schemas attempts to convert the input value to the appropriate type.

const schema = z.coerce.string();
 
schema.parse("tuna");    // => "tuna"
schema.parse(42);        // => "42"
schema.parse(true);      // => "true"
schema.parse(null);      // => "null"

Literals

Literal schemas represent a literal type, like "hello world" or 5.

const tuna = z.literal("tuna");
const twelve = z.literal(12);
const twobig = z.literal(2n);
const tru = z.literal(true);
const terrific = z.literal(Symbol("terrific"));

To represent the JavaScript literals null and undefined:

z.null();
z.undefined();
z.void(); // equivalent to z.undefined()

To allow multiple literal values:

const colors = z.literal(["red", "green", "blue"]);
 
colos.parse("green"); // ✅
colors.parse("yellow"); // ❌

To extract the set of allowed values from a literal schema:

colors.values; // => Set<"red" | "green" | "blue">

Strings

Zod provides a handful of built-in string validation and transform APIs. To perform some common string validations:

z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().regex(/^[a-z]+$/);
z.string().startsWith("aaa");
z.string().endsWith("zzz");
z.string().includes("---");
z.string().uppercase();
z.string().lowercase();

To perform some simple string transforms:

z.string().trim(); // trim whitespace
z.string().toLowerCase(); // toLowerCase
z.string().toUpperCase(); // toUpperCase

String formats

To validate against some common string formats:

z.email();
z.uuid();
z.url();
z.emoji();         // validates a single emoji character
z.base64();
z.base64url();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.cidrv4();        // ipv4 CIDR block
z.cidrv6();        // ipv6 CIDR block
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();

Emails

To validate email addresses:

z.email();

By default, Zod uses a comparatively strict email regex designed to validate normal email addresses containing common characters. It's roughly equivalent to the rules enforced by Gmail. To learn more about this regex, refer to this post.

/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i

To customize the email validation behavior, you can pass a custom regular expression to the pattern param.

z.email({ pattern: /your regex here/ });

Zod exports several useful regexes you could use.

// Zod's default email regex
z.email();
z.email({ pattern: z.regexes.email }); // equivalent
 
// the regex used by browsers to validate input[type=email] fields
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
 
// the classic emailregex.com regex (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
 
// a loose regex that allows Unicode (good for intl emails)
z.email({ pattern: z.regexes.unicodeEmail });

UUIDs

To validate UUIDs:

z.uuid();

To specify a particular UUID version:

// supports "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"
z.uuid({ version: "v4" });
 
// for convenience
z.uuidv4();
z.uuidv6();
z.uuidv7();

The RFC 4122 UUID spec requires the first two bits of byte 8 to be 10. Other UUID-like identifiers do not enforce this constraint. To validate any UUID-like identifier:

z.guid();

ISO datetimes

As you may have noticed, Zod string includes a few date/time related validations. These validations are regular expression based, so they are not as strict as a full date/time library. However, they are very convenient for validating user input.

The z.iso.datetime() method enforces ISO 8601; by default, no timezone offsets are allowed:

const datetime = z.iso.datetime();
 
datetime.parse("2020-01-01T00:00:00Z"); // ✅
datetime.parse("2020-01-01T00:00:00.123Z"); // ✅
datetime.parse("2020-01-01T00:00:00.123456Z"); // ✅ (arbitrary precision)
datetime.parse("2020-01-01T00:00:00+02:00"); // ❌ (no offsets allowed)

To allow timesone offsets:

const datetime = z.iso.datetime({ offset: true });
 
datetime.parse("2020-01-01T00:00:00+02:00"); // ✅
datetime.parse("2020-01-01T00:00:00.123+02:00"); // ✅ (millis optional)
datetime.parse("2020-01-01T00:00:00.123+0200"); // ✅ (millis optional)
datetime.parse("2020-01-01T00:00:00.123+02"); // ✅ (only offset hours)
datetime.parse("2020-01-01T00:00:00Z"); // ✅ (Z still supported)

To allow unqualified (timezone-less) datetimes:

const schema = z.iso.datetime({ local: true });
schema.parse("2020-01-01T00:00:00"); // ✅

To constrain the allowable precision (by default, arbitrary sub-second precision is supported).

const datetime = z.iso.datetime({ precision: 3 });
 
datetime.parse("2020-01-01T00:00:00.123Z"); // ✅
datetime.parse("2020-01-01T00:00:00Z"); // ❌
datetime.parse("2020-01-01T00:00:00.123456Z"); // ❌

ISO dates

The z.iso.date() method validates strings in the format YYYY-MM-DD.

const date = z.iso.date();
 
date.parse("2020-01-01"); // ✅
date.parse("2020-1-1"); // ❌
date.parse("2020-01-32"); // ❌

ISO times

Added in Zod 3.23

The z.iso.time() method validates strings in the format HH:MM:SS[.s+]. The second can include arbitrary decimal precision. It does not allow timezone offsets of any kind.

const time = z.iso.time();
 
time.parse("00:00:00"); // ✅
time.parse("09:52:31"); // ✅
time.parse("23:59:59.9999999"); // ✅ (arbitrary precision)
 
time.parse("00:00:00.123Z"); // ❌ (no `Z` allowed)
time.parse("00:00:00.123+02:00"); // ❌ (no offsets allowed)

You can set the precision option to constrain the allowable decimal precision.

const time = z.iso.time({ precision: 3 });
 
time.parse("00:00:00.123"); // ✅
time.parse("00:00:00.123456"); // ❌
time.parse("00:00:00"); // ❌

IP addresses

const ipv4 = z.ipv4();
v4.parse("192.168.0.0"); // ✅
 
const ipv6 = z.ipv6();
v6.parse("2001:db8:85a3::8a2e:370:7334"); // ✅

IP blocks (CIDR)

Validate IP address ranges specified with CIDR notation. By default, .cidr() allows both IPv4 and IPv6.

const cidrv4 = z.string().cidrv4();
cidrv4.parse("192.168.0.0/24"); // ✅
 
const cidrv6 = z.string().cidrv6();
cidrv6.parse("2001:db8::/32"); // ✅

Numbers

Use z.number() to validate numbers. It allows any finite number.

const schema = z.number();
 
schema.parse(3.14);      // ✅
schema.parse(NaN);       // ❌
schema.parse(Infinity);  // ❌

Zod implements a handful of number-specific validations:

z.number().gt(5);
z.number().gte(5);                     // alias .min(5)
z.number().lt(5);
z.number().lte(5);                     // alias .max(5)
z.number().positive();       
z.number().nonnegative();    
z.number().negative(); 
z.number().nonpositive(); 
z.number().multipleOf(5);              // alias .step(5)

If (for some reason) you want to validate NaN, use z.nan().

z.nan().parse(NaN);              // ✅
z.nan().parse("anything else");  // ❌

Integers

To validate integers:

z.int();     // restricts to safe integer range
z.int32();   // restrict to int32 range

BigInts

To validate BigInts:

z.bigint();

Zod includes a handful of bigint-specific validations.

z.bigint().gt(5n);
z.bigint().gte(5n);                    // alias `.min(5n)`
z.bigint().lt(5n);
z.bigint().lte(5n);                    // alias `.max(5n)`
z.bigint().positive(); 
z.bigint().nonnegative(); 
z.bigint().negative(); 
z.bigint().nonpositive(); 
z.bigint().multipleOf(5n);             // alias `.step(5n)`

Booleans

To validate boolean values:

z.boolean().parse(true); // => true
z.boolean().parse(false); // => false

Dates

Use z.date() to validate Date instances.

z.date().safeParse(new Date()); // success: true
z.date().safeParse("2022-01-12T00:00:00.000Z"); // success: false

To customize the error message:

z.date({
  error: issue => issue.input === undefined ? "Required" : "Invalid date"
});

Zod provides a handful of date-specific validations.

z.date().min(new Date("1900-01-01"), { error: "Too old!" });
z.date().max(new Date(), { error: "Too young!" });

Enums

Use z.enum to validate inputs against a fixed set of allowable string values.

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
 
FishEnum.parse("Salmon"); // => "Salmon"
FishEnum.parse("Swordfish"); // => ❌

Careful — If you declare your string array as a variable, Zod won't be able to properly infer the exact values of each element.

const fish = ["Salmon", "Tuna", "Trout"];
 
const FishEnum = z.enum(fish);
type FishEnum = z.infer<typeof FishEnum>; // string

To fix this, always pass the array directly into the z.enum() function, or use as const.

const fish = ["Salmon", "Tuna", "Trout"] as const;
 
const FishEnum = z.enum(fish);
type FishEnum = z.infer<typeof FishEnum>; // "Salmon" | "Tuna" | "Trout"

You can also pass in an externally-declared TypeScript enum.

Zod 4 — This replaces the z.nativeEnum() API in Zod 3.

Note that using TypeScript's enum keyword is not recommended.

enum Fish {
  Salmon = "Salmon",
  Tuna = "Tuna",
  Trout = "Trout",
}
 
const FishEnum = z.enum(Fish);

.enum

To extract the schema's values as an enum-like object:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
 
FishEnum.enum;
// => { Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout" }

.exclude()

To create a new enum schema, excluding certain values:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const SalmonAndTroutOnly = FishEnum.extract(["Salmon", "Trout"]);

.extract()

To create a new enum schema, extracting certain values:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const TunaOnly = FishEnum.exclude(["Salmon", "Trout"]);

Stringbool

💎 New in Zod 4

In some cases (e.g. parsing environment variables) it's valuable to parse certain string "boolish" values to a plain boolean value. To support this, Zod 4 introduces z.stringbool():

const strbool = z.stringbool();
 
strbool.parse("true")         // => true
strbool.parse("1")            // => true
strbool.parse("yes")          // => true
strbool.parse("on")           // => true
strbool.parse("y")            // => true
strbool.parse("enable")       // => true
 
strbool.parse("false");       // => false
strbool.parse("0");           // => false
strbool.parse("no");          // => false
strbool.parse("off");         // => false
strbool.parse("n");           // => false
strbool.parse("disabled");    // => false
 
strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]>

To customize the truthy and falsy values:

z.stringbool({
  truthy: ["yes", "true"],
  falsy: ["no", "false"]
})

Be default the schema is case-insensitive; all inputs are converted to lowercase before comparison to the truthy/falsy values. To make it case-sensitive:

z.stringbool({
  case: "sensitive"
});

Optionals

To make a schema optional (that is, to allow undefined inputs).

z.optional(z.literal("yoda")); // or z.literal("yoda").optional()

This returns a ZodOptional instance that wraps the original schema. To extract the inner schema:

optionalYoda.unwrap(); // ZodLiteral<"yoda">

Nullables

To make a schema nullable (that is, to allow null inputs).

z.nullable(z.literal("yoda")); // or z.literal("yoda").nullable()

This returns a ZodNullable instance that wraps the original schema. To extract the inner schema:

nullableYoda.unwrap(); // ZodLiteral<"yoda">

Nullish

To make a schema nullish (both optional and nullable):

const nullishYoda = z.nullish(z.literal("yoda"));

Refer to the TypeScript manual for more about the concept of nullish.

Unknown

Zod aims to mirror TypeScript's type system one-to-one. As such, Zod provides APIs to represent the following special types:

// allows any values
z.any(); // inferred type: `any`
z.unknown(); // inferred type: `unknown`

Never

No value will pass validation.

z.never(); // inferred type: `never`

Template literals

💎 New in Zod 4

Zod 4 finally implements one of the last remaining unrepresented features of TypeScript's type system: template literals. Virtually all primitive schemas can be used in z.templateLiteral: strings, string formats like z.email(), numbers, booleans, enums, literals (of the non-template variety), optional/nullable, and other template literals.

const hello = z.templateLiteral(["hello, ", z.string()]);
// `hello, ${string}`
 
const cssUnits = z.enum(["px", "em", "rem", "%"]);
const css = z.templateLiteral([z.number(), cssUnits ]);
// `${number}px` | `${number}em` | `${number}rem` | `${number}%`
 
const email = z.templateLiteral([
  z.string().min(1),
  "@",
  z.string().max(64),
]);
// `${string}@${string}` (the min/max refinements are enforced!)

Objects

There are two APIs for defining object schemas: z.object() and z.interface(). The two APIs are largely identical, with a small (but important!) difference in how optional properties are defined.

To define an object type:

// all properties are required by default
const Person = z.interface({
  name: z.string(),
  age: z.number(),
});
 
type Person = z.infer<typeof Person>;
// => { name: string; age: number; }

By default, all properties are required. To make certain properties optional:

const Dog = z.interface({
  name: z.string(),
  "age?": z.number(), // optional field
});
 
Dog.parse({ name: "Yeller" }); // ✅

By default, unrecognized keys are stripped from the parsed result:

Dog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller" }

To define a strict schema that throws an error when unknown keys are found:

const StrictDog = z.strictInterface({
  name: z.string(),
});
 
StrictDog.parse({ name: "Yeller", extraKey: true });
// ❌ throws

To define a loose schema that allows unknown keys to pass through:

const LooseDog = z.looseInterface({
  name: z.string(),
});
 
Dog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller", extraKey: true }

.shape

To access the internal schemas:

Dog.shape.name; // => string schema
Dog.shape.age; // => number schema

.keyof()

To create a ZodEnum schema from the keys of an object schema:

const keySchema = Dog.keyof();
// => ZodEnum<["name", "age"]>

.extend()

To add additional fields to an object schema:

const DogWithBreed = Dog.extend({
  breed: z.string(),
});

This API can be used to overwrite existing fields! Be careful with this power!

You can also pass another object schema into .extend():

const HasBreed = z.interface({ breed: z.string() });
const DogWithBreed = Dog.extend(HasBreed);

If the two schemas share keys, B will override A. The returned schema also inherits the strictness level (strip, strict, loose) from B.

.pick

Inspired by TypeScript's built-in Pick and Omit utility types, Zod provides dedicated APIs for picking and omitting certain keys from an object schema.

Starting from this initial schema:

const Recipe = z.interface({
  title: z.string(),
  "description?": z.string(),
  ingredients: z.array(z.string()),
});
// { name: string; description?: string; ingredients: string[] }

To pick certain keys:

const JustTheTitle = Recipe.pick({ title: true });

.omit

To omit certain keys:

const RecipeNoId = Recipe.omit({ id: true });

.partial()

For convenience, Zod provides a dedicated API for making some or all properties optional, inspired by the built-in TypeScript utility type Partial.

To make all fields optional:

const PartialRecipe = Recipe.partial();
// { title?: string | undefined; description?: string | undefined; ingredients?: string[] | undefined }

To make certain properties optional:

const RecipeOptionalIngredients = Recipe.partial({
  ingredients: true,
});
// { title: string; description?: string | undefined; ingredients?: string[] | undefined }

.required()

Zod provides an API for making some or all properties required, inspired by TypeScript's Required utility type.

To make all properties required:

const RequiredRecipe = Recipe.required();
// { title: string; description: string; ingredients: string[] }

To make certain properties required:

const RecipeRequiredDescription = Recipe.required({description: true});
// { title: string; description: string; ingredients: string[] }

Recursive objects

The z.interface() API is capable of representing recursive types using getters.

const Category = z.interface({
  name: z.string(),
  get subcategories(){
    return z.array(Category)
  }
});
 
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }

You can also represent mutually recursive types:

const User = z.interface({
  email: z.email(),
  get posts(){
    return z.array(Post)
  }
});
 
const Post = z.interface({
  title: z.string(),
  get author(){
    return User
  }
});

All object APIs (pick, omit, required, partial, etc) work as you'd expect.

Though recursive schemas are supported, passing cyclical data into Zod will cause an infinite loop.

To detect cyclical objects before they cause problems, consider this approach.

Arrays

To define an array schema:

const stringArray = z.array(z.string()); // or z.string().array()

To access the inner schema for an element of the array.

stringArray.unwrap(); // => string schema

.min/.max/.length

z.array(z.string()).min(5); // must contain 5 or more items
z.array(z.string()).max(5); // must contain 5 or fewer items
z.array(z.string()).length(5); // must contain 5 items exactly

Tuples

Unlike arrays, tuples are typically fixed-length arrays that specify different schemas for each index.

const MyTuple = z.tuple([
  z.string(),
  z.number(),
  z.boolean()
]);
 
type MyTuple = z.infer<typeof MyTuple>;
// [string, number, boolean]

To add a variadic ("rest") argument:

const variadicTuple = z.tuple([z.string()], z.number());
// => [string, ...number[]];

Unions

Union types (A | B) represent a logical "OR". Zod union schemas will check the input against each option in order. The first value that validates successfully is returned.

const stringOrNumber = z.union([z.string(), z.number()]);
// string | number
 
stringOrNumber.parse("foo"); // passes
stringOrNumber.parse(14); // passes

To extract the internal option schemas:

stringOrNumber.options; // [ZodString, ZodNumber]

Discriminated unions

A discriminated union is a special kind of union in which a) all the options are object schemas that b) share a particular key (the "discriminator"). Based on the value of the discriminator key, TypeScript is able to "narrow" the type signature as you'd expect.

type MyResult =
  | { status: "success"; data: string }
  | { status: "failed"; error: string };
 
function handleResult(result: MyResult){
  if(result.status === "success"){
    result.data; // string
  } else {
    result.error; // string
  }
}

You could represent with with a regular z.union(). But regular unions are naive—they check the input against each option in order and return the first one that passes. This can be slow for large unions.

So Zod provides a z.discriminatedUnion() API that uses a discriminator key to make parsing more efficient.

const MyResult = z.discriminatedUnion([
  z.interface({ status: z.literal("success"), data: z.string() }),
  z.interface({ status: z.literal("failed"), error: z.string() }),
]);

In Zod 3, you were required to specify the discriminator key as the first argument. This is no longer necessary, as Zod can now automatically detect the discriminator key.

const MyResult = z.discriminatedUnion("status", [
  z.interface({ status: z.literal("success"), data: z.string() }),
  z.interface({ status: z.literal("failed"), error: z.string() }),
]);

If Zod can't find a discriminator key, it will throw an error at schema creation time.

Intersections

Intersection types (A & B) represent a logical "AND".

const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
 
type c = z.infer<typeof c>; // => number

This can be useful for intersecting two object types.

const Person = z.intersection({ name: z.string() });
type Person = z.infer<typeof Person>;
 
const Employee = z.intersection({ role: z.string() });
type Employee = z.infer<typeof Employee>;
 
const EmployedPerson = z.intersection(Person, Employee);
type EmployedPerson = z.infer<typeof EmployedPerson>;
// Person & Employee

In most cases, it is better to use A.extend(B) to merge two object schemas. This approach returns a new object schema, whereas z.intersection(A, B) returns a ZodIntersection instance which lacks common object methods like pick and omit.

Records

Record schemas are used to validate types such as Record<string, number>.

const IdCache = z.record(z.string(), z.string());
 
IdCache.parse({
  carlotta: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
  jimmie: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
});

The key schema can be any Zod schema that as assignable to string | number | symbol.

const Keys = z.union([z.string(), z.number(), z.symbol()]);
const AnyObject = z.record(Keys, z.unknown());
// Record<string | number | symbol, unknown>

To create an object schemas containing keys defined by an enum:

const Keys = z.enum(["id", "name", "email"]);
const Person = z.record(Keys, z.string());

Maps

const StringNumberMap = z.map(z.string(), z.number());
type StringNumberMap = z.infer<typeof StringNumberMap>; // Map<string, number>
 
const myMap: StringNumberMap = new Map();
myMap.set("one", 1);
myMap.set("two", 2);
 
StringNumberMap.parse(myMap);

Sets

const NumberSet = z.set(z.number());
type NumberSet = z.infer<typeof NumberSet>; // Set<number>
 
const mySet: NumberSet = new Set();
mySet.add(1);
mySet.add(2);
NumberSet.parse(mySet);

Set schemas can be further constrained with the following utility methods.

z.set(z.string()).min(5); // must contain 5 or more items
z.set(z.string()).max(5); // must contain 5 or fewer items
z.set(z.string()).size(5); // must contain 5 items exactly

Promises

Deprecatedz.promise() is deprecated in Zod 4. There are vanishingly few valid uses cases for a Promise schema. If you suspect a value might be a Promise, simply await it before parsing it with Zod.

Instanceof

You can use z.instanceof to check that the input is an instance of a class. This is useful to validate inputs against classes that are exported from third-party libraries.

class Test {
  name: string;
}
 
const TestSchema = z.instanceof(Test);
 
TestSchema.parse(new Test()); // ✅
TestSchema.parse("whatever"); // ❌

Refinements

Every Zod schema stores an array of refinements. Refinements are a way to perform custom validation that can Zod doesn't provide a native API for.

.refine()

const myString = z.string().refine((val) => val.length <= 255);

Refinement functions should never throw. Instead they should return a falsy value to signal failure. Thrown errors are not caught by Zod.

To customize the error message:

const myString = z.string().refine((val) => val.length > 8, { 
  error: "Too short!" 
});

By default, validation issues from checks are considered continuable; that is, Zod will execute all checks in sequence, even if one of them causes a validation error. This is usually desirable, as it means Zod can surface as many errors as possible in one go.

const myString = z.string()
  .refine((val) => val.length > 8)
  .refine((val) => val === val.toLowerCase());
  
 
const result = myString.safeParse("OH NO");
result.error.issues;
/* [
  { "code": "custom", "message": "Too short!" },
  { "code": "custom", "message": "Must be lowercase" }
] */

To mark a particular refinement as non-continuable, use the abort parameter. Validation will terminate if the check fails.

const myString = z.string()
  .refine((val) => val.length > 8, { abort: true })
  .refine((val) => val === val.toLowerCase());
 
 
const result = myString.safeParse("OH NO");
result.error!.issues;
// => [{ "code": "custom", "message": "Too short!" }]

To customize the error path, use the path parameter. This is typically only useful in the context of object schemas.

const passwordForm = z
  .object({
    password: z.string(),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ["confirm"], // path of error
  });

This will set the path parameter in the associated issue:

const result = passwordForm.safeParse({ password: "asdf", confirm: "qwer" });
result.error.issues;
/* [{
  "code": "custom",
  "path": [ "confirm" ],
  "message": "Passwords don't match"
}] */

Refinements can be async:

const userId = z.string().refine(async (id) => {
  // verify that ID exists in database
  return true;
});

If you use async refinements, you must use the .parseAsync method to parse data! Otherwise Zod will throw an error.

const result = await userId.parseAsync("abc123");

.superRefine()

In Zod 4, .superRefine() has been deprecated in favor of .check()

.check()

The .refine() API is syntactic sugar atop a more versatile (and verbose) API called .check(). You can use this API to create multiple issues in a single refinement or have full control of the generated issue objects.

const UniqueStringArray = z.array(z.string()).check((ctx) => {
  if (ctx.value.length > 3) {
    ctx.issues.push({
      code: "too_big",
      maximum: 3,
      origin: "array",
      inclusive: true,
      message: "Too many items 😡",
      input: ctx.value
    });
  }
 
  if (ctx.value.length !== new Set(ctx.value).size) {
    ctx.issues.push({
      code: "custom",
      message: `No duplicates allowed.`,
      input: ctx.value,
      continue: true // make this issue continuable (default: false)
    });
  }
});

The regular .refine API only generates issues with a "custom" error code, but .check() makes it possible to throw other issue types. For more information on Zod's internal issue types, read the Error customization docs.

Pipes

Schemas can be chained together into "pipes". Pipes are primarily useful when used in conjunction with Transforms.

const stringToLength = z.string().pipe(z.transform(val => val.length));
 
stringToLength.parse("hello"); // => 5

Transforms

Transforms are a special kind of schema. Instead of validating input, they accept anything and perform some transformation on the data. To define a transform:

const castToString = z.transform((val) => String(val));
 
castToString.parse("asdf"); // => "asdf"
castToString.parse(123); // => "123"
castToString.parse(true); // => "true"

To perform validation logic inside a transform, use ctx. To report a validation issue, push a new issue onto ctx.issues (similar to the .check() API).

const coercedInt = z.transform((val, ctx) => {
  try {
    const parsed = Number.parseInt(String(val));
    return parsed;
  } catch (e) {
    ctx.issues.push({
      code: "custom",
      message: "Not a number",
      input: val,
    });
 
    // this is a special constant with type `never`
    // return it to exit the transform without impacting the inferred return type
    return z.NEVER; 
  }
});

Most commonly, transforms are used in conjunction with Pipes. This combination is useful for performing some initial validation, then transforming the parsed data into another form.

const stringToLength = z.string().pipe(z.transform(val => val.length));
 
stringToLength.parse("hello"); // => 5

.transform()

Piping some schema into a transform is a common pattern, so Zod provides a convenience .transform() method.

const stringToLength = z.string().transform(val => val.length); 

Transforms can also be async:

const idToUser = z
  .string()
  .transform(async (id) => {
    // fetch user from database
    return db.getUserById(id); 
  });
 
const user = await idToUser.parseAsync("abc123");

If you use async transforms, you must use a .parseAsync or .safeParseAsync when parsing data! Otherwise Zod will throw an error.

.preprocess()

Piping a transform into another schema is another common pattern, so Zod provides a convenience z.preprocess() function.

Defaults

To set a default value for a schema:

const defaultTuna = z.string().default("tuna");
 
defaultTuna.parse(undefined); // => "tuna"

Alternatively, you can pass a function which will be re-executed whenever a default value needs to be generated:

const randomDefault = z.number().default(Math.random);
 
randomDefault.parse(undefined);    // => 0.4413456736055323
randomDefault.parse(undefined);    // => 0.1871840107401901
randomDefault.parse(undefined);    // => 0.7223408162401552

Catch

Use .catch() to define a fallback value to be returned in the event of a validation error:

const numberWithCatch = z.number().catch(42);
 
numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42

Alternatively, you can pass a function which will be re-executed whenever a catch value needs to be generated.

const numberWithRandomCatch = z.number().catch((ctx) => {
  ctx.error; // the caught ZodError
  return Math.random();
});
 
numberWithRandomCatch.parse("sup"); // => 0.4413456736055323
numberWithRandomCatch.parse("sup"); // => 0.1871840107401901
numberWithRandomCatch.parse("sup"); // => 0.7223408162401552

Branded types

TypeScript's type system is structural, meaning that two types that are structurally equivalent are considered the same.

type Cat = { name: string };
type Dog = { name: string };
 
const pluto: Dog = { name: "pluto" };
const simba: Cat = fido; // works fine

In some cases, it can be desirable to simulate nominal typing inside TypeScript. This can be achieved with branded types (also known as "opaque types").

const Cat = z.object({ name: z.string() }).brand<"Cat">();
const Dog = z.object({ name: z.string() }).brand<"Dog">();
 
type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">
type Dog = z.infer<typeof Dog>; // { name: string } & z.$brand<"Dog">
 
const pluto = Dog.parse({ name: "pluto" });
const simba: Cat = pluto; // ❌ not allowed

Under the hood, this works by attaching a "brand" to the schema's inferred type.

const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">

With this brand, any plain (unbranded) data structures are no longer assignable to the inferred type. You have to parse some data with the schema to get branded data.

Note that branded types do not affect the runtime result of .parse. It is a static-only construct.

Readonly

To mark a schema as readonly:

const ReadonlyUser = z.object({ name: z.string() }).readonly();
type ReadonlyUser = z.infer<typeof ReadonlyUser>;
// Readonly<{ name: string }>

This returns a new schema that wraps the original. The new schema's inferred type will be marked as readonly. Note that this only affects objects, arrays, tuples, Set, and Map in TypeScript:

z.object({ name: z.string() }).readonly(); // { readonly name: string }
z.array(z.string()).readonly(); // readonly string[]
z.tuple([z.string(), z.number()]).readonly(); // readonly [string, number]
z.map(z.string(), z.date()).readonly(); // ReadonlyMap<string, Date>
z.set(z.string()).readonly(); // ReadonlySet<string>

Inputs will be parsed using the original schema, then the result will be frozen with Object.freeze() to prevent modifications.

const result = ReadonlyUser.parse({ name: "fido" });
result.name = "simba"; // throws TypeError

Template literals

New in Zod 4

To define a template literal schema:

const schema = z.templateLiteral("hello, ", z.string(), "!");
// `hello, ${string}!`

The z.templateLiteral API can handle any number of string literals (e.g. "hello") and schemas. Any schema with an inferred type that's assignable to string | number | bigint | boolean | null | undefined can be passed.

z.templateLiteral([ "hi there" ]); 
// `hi there`
 
z.templateLiteral([ "email: ", z.string()]); 
// `email: ${string}`
 
z.templateLiteral([ "high", z.literal(5) ]); 
// `high5`
 
z.templateLiteral([ z.nullable(z.literal("grassy")) ]); 
// `grassy` | `null`
 
z.templateLiteral([ z.number(), z.enum(["px", "em", "rem"]) ]); 
// `${number}px` | `${number}em` | `${number}rem`

JSON

To validate any JSON-encodable value:

const jsonSchema = z.json();

This is a convenience API that returns the following union schema:

const jsonSchema = z.lazy(() => {
  return z.union([
    z.string(params), 
    z.number(), 
    z.boolean(), 
    z.null(), 
    z.array(jsonSchema), 
    z.record(z.string(), jsonSchema)
  ]);
});

Custom

You can create a Zod schema for any TypeScript type by using z.custom(). This is useful for creating schemas for types that are not supported by Zod out of the box, such as template string literals.

const px = z.custom<`${number}px`>((val) => {
  return typeof val === "string" ? /^\d+px$/.test(val) : false;
});
 
type px = z.infer<typeof px>; // `${number}px`
 
px.parse("42px"); // "42px"
px.parse("42vw"); // throws;

If you don't provide a validation function, Zod will allow any value. This can be dangerous!

z.custom<{ arg: string }>(); // performs no validation

You can customize the error message and other options by passing a second argument. This parameter works the same way as the params parameter of .refine.

z.custom<...>((val) => ..., "custom error message");