Skip to main content
This guide walks you through the core concepts of Zod: defining schemas, parsing data, type inference, and error handling.

Schema Definition

A schema defines the shape and validation rules for your data. Start by defining a simple object schema:
import * as z from "zod";

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});
You can build more complex schemas by composing primitive types:
const User = z.object({
  name: z.string(),
  age: z.number(),
  email: z.optional(z.string()),
  isAdmin: z.boolean(),
});
Schemas are immutable. Methods like .check() and .optional() return new schema instances.

Parsing Data

1

Use .parse() for validation

The .parse() method validates input and returns a deep clone if valid. It throws a ZodError if validation fails:
const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

// Valid input
const result = Player.parse({ username: "billie", xp: 100 });
console.log(result);
// => { username: "billie", xp: 100 }
2

Handle validation errors

When parsing fails, Zod throws a detailed error:
try {
  Player.parse({ username: 42, xp: "100" });
} catch (err) {
  if (err instanceof z.ZodError) {
    console.log(err.issues);
    /* [
      {
        expected: 'string',
        code: 'invalid_type',
        path: [ 'username' ],
        message: 'Invalid input: expected string'
      },
      {
        expected: 'number',
        code: 'invalid_type',
        path: [ 'xp' ],
        message: 'Invalid input: expected number'
      }
    ] */
  }
}
3

Use .safeParse() to avoid exceptions

For error handling without try/catch, use .safeParse(). It returns a discriminated union:
const result = Player.safeParse({ username: 42, xp: "100" });

if (!result.success) {
  // result.error is a ZodError instance
  console.log(result.error.issues);
} else {
  // result.data has the correct type: { username: string; xp: number }
  console.log(result.data);
}

Type Inference

Zod automatically infers TypeScript types from your schemas using z.infer<> or z.output<>:
const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

// Extract the TypeScript type
type Player = z.infer<typeof Player>;
// type Player = {
//   username: string;
//   xp: number;
// }

// Use the inferred type
const player: Player = { username: "billie", xp: 100 };
For schemas with transformations, use z.input<> to get the input type and z.output<> (or z.infer<>) to get the output type.

Input vs Output Types

When you use transforms, input and output types can differ:
const StringToNumber = z.string().check(
  z.refine((val) => !isNaN(Number(val)))
);

type Input = z.input<typeof StringToNumber>;  // string
type Output = z.output<typeof StringToNumber>; // string

Error Handling

Zod provides detailed error information when validation fails.

Working with ZodError

Every failed parse produces a ZodError with an issues array:
const result = z.string().safeParse(123);

if (!result.success) {
  console.log(result.error.issues);
  // [{
  //   code: 'invalid_type',
  //   expected: 'string',
  //   received: 'number',
  //   path: [],
  //   message: 'Invalid input'
  // }]
}

Custom Error Messages

You can provide custom error messages when defining schemas:
const Email = z.email("Please provide a valid email address");

const result = Email.safeParse("invalid-email");
if (!result.success) {
  console.log(result.error.issues[0].message);
  // => "Please provide a valid email address"
}
Or use a function for dynamic messages:
const Email = z.email({
  error: () => "Please provide a valid email address"
});

Complete Example

Here’s a complete example combining all concepts:
import * as z from "zod";

// Define the schema
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  email: z.optional(z.email("Invalid email format")),
});

// Infer the TypeScript type
type User = z.infer<typeof UserSchema>;

// Parse some data
const input = {
  name: "John Doe",
  age: 30,
  email: "[email protected]",
};

const result = UserSchema.safeParse(input);

if (result.success) {
  // Type-safe access to validated data
  const user: User = result.data;
  console.log(`Welcome, ${user.name}!`);
} else {
  // Handle validation errors
  console.error("Validation failed:");
  result.error.issues.forEach((issue) => {
    console.error(`- ${issue.path.join('.')}: ${issue.message}`);
  });
}

Next Steps

Primitives

Explore all primitive types: strings, numbers, dates, and more

Objects

Learn about object schemas, nesting, and composition

Arrays & Tuples

Work with arrays, tuples, and collections

Advanced Types

Master unions, intersections, and discriminated unions

Build docs developers (and LLMs) love