Zod Showcase

Core Basics

The fundamental building blocks of Zod. Every schema you write will use some combination of these core concepts. The examples below show how a schema is defined, how it validates JSON input, and how TypeScript types are derived from the schema. Feel free to change the data in the JSON inputs and see how the validation results change.

Primitive Types

Zod validates each field against its declared type. Zod Primitives: string, number, int, bigint, boolean, symbol, undefined and null. Try changing age in the example JSON input to a string or removing active to see what errors occur.

Schema
import { z } from 'zod';

const schema = z.object({
  name: z.string(),
  age: z.number(),
  active: z.boolean(),
});
Inferred Type
type Schema = {
  name: string;
  age: number;
  active: boolean;
}
Output
{
  "name": "Alice",
  "age": 30,
  "active": true
}

Validation Rules

With Zod you can chain different validation methods. A key thing to notice: .min() and .max() mean character count on strings, but numeric value on numbers. Strings also support .regex() for pattern matching, .length() for an exact length, and .startsWith() / .endsWith(). Numbers support .int() to reject decimals, .positive() / .negative() and .multipleOf() for step validation. In the JSON input try setting username to "ab" (too short), age to 12 (too young), or rating to 4.3 (not a 0.5 step) to see the different errors.

Schema
import { z } from 'zod';

const schema = z.object({
  // min and max on a string means character count
  username: z.string().min(3).max(20).regex(/^[a-z0-9_]+$/),

  // min and max on a number means numeric value
  age: z.int().min(13).max(120),

  // exact length and digit-only pattern
  zipCode: z.string().length(4).regex(/^\d+$/),

  // any 0.5 increment between 0 and 5
  rating: z.number().min(0).max(5).multipleOf(0.5),
});
Inferred Type
type Schema = {
  username: string;
  age: number;
  zipCode: string;
  rating: number;
}
Output
{
  "username": "alice_dev",
  "age": 25,
  "zipCode": "5058",
  "rating": 4.5
}

Nested Objects

Zod supports nesting objects and validates them recursively. Try setting email in the example JSON input to a non-email string to see built-in string validator errors.

Schema
import { z } from 'zod';

const schema = z.object({
  user: z.object({
    id: z.number(),
    email: z.email(),
  }),
});
Inferred Type
type Schema = {
  user: {
    id: number;
    email: string;
  };
}
Output
{
  "user": {
    "id": 1,
    "email": "alice@example.com"
  }
}

Arrays

z.array() validates that a value is an array and that every element matches the item schema. We can chain .min() and .max() to constrain the array length. Try adding a number to tags in the example JSON input, or removing all items to see the minimum-length error.

Schema
import { z } from 'zod';

const schema = z.object({
  // At least one tag, at most 3, containing non-empty strings
  tags: z.array(z.string().min(1)).min(1).max(3),
  scores: z.array(z.number()),
});
Inferred Type
type Schema = {
  tags: string[];
  scores: number[];
}
Output
{
  "tags": [
    "svelte",
    "zod"
  ],
  "scores": [
    95,
    87,
    100
  ]
}

Optional, Nullable, and Default

.optional() means the field may be absent entirely. .nullable() means the field must be present but can be null. .default() fills in a value when the field is absent. The caller can omit it, but it is always present in the parsed output. Try removing theme in the example JSON input to see the default take effect.

Schema
import { z } from 'zod';

const schema = z.object({
  name: z.string(),
  // Field can be absent from the input
  nickname: z.string().optional(),
  // Field must be present, but can be null
  avatar: z.string().nullable(),
  // Absent input that defaults to 'light' in the output
  theme: z.string().optional().default('light'),
});
Inferred Type
type Schema = {
  name: string;
  nickname?: string | undefined;
  avatar: string | null;
  theme: string;  // always present since default fills it in
}
Output
{
  "name": "Alice",
  "avatar": null,
  "theme": "dark"
}

Multi-value Literals

z.literal() represent a literal type, for instance "Hello world", 5 or true. It accepts an array of values, making it easy to validate that a field is one of a fixed set. The schema accepts any of the listed values exactly and rejects anything else. Try changing role in the example JSON input to superuser.

Schema
import { z } from 'zod';

const schema = z.object({
  role: z.literal(['admin', 'user', 1337]),
});
Inferred Type
type Schema = {
  role: "admin" | "user" | 1337;
}
Output
{
  "role": "admin"
}

Enums

z.enum() validates that a value is one of a fixed set of string values and infers a precise union type. Use it for status fields, roles, categories. Anything backed by a <select> or a fixed list. Try changing status to "archived" to see it fail. The error output is similar to z.literal(), but limited to only string values. As a bonus you can access the allowed values at runtime via i.e. schema.priority.enum => { low: "low", medium: "medium", high: "high" }.

Schema
import { z } from 'zod';

const schema = z.object({
  status: z.enum(['draft', 'published', 'scheduled']),
  priority: z.enum(['low', 'medium', 'high']),
});

// Access the allowed values at runtime:
// schema.shape.status.options
// → ['draft', 'published', 'scheduled']
Inferred Type
type Schema = {
  status: 'draft' | 'published' | 'scheduled';
  priority: 'low' | 'medium' | 'high';
}
Output
{
  "status": "published",
  "priority": "high"
}

Strict and Loose Objects

By default, z.object() silently strips unknown keys. z.strictObject() rejects extra keys with an error. This can be useful when you want to catch unexpected fields. On the other end z.looseObject() passes extra keys through unchanged. Try adding a "role": "admin" field to the user in the example JSON input to see the strict schema reject it. Notice how settings already has an extra "language" key in the input, it is passed directly to the output with no validation or warning.

Schema
import { z } from 'zod';

const Schema = z.object({
  // Extra keys throw an error
  user: z.strictObject({
    name: z.string(),
    age: z.number(),
  }),

  // Extra keys pass through unchanged
  settings: z.looseObject({
    theme: z.string(),
    notifications: z.boolean(),
  }),

  // Unknown keys on z.object() are silently stripped
});
Inferred Type
type Schema = {
  // Fails if extra keys are present
  user: { name: string; age: number }

  // Extra keys survive, but aren't typed
  settings: { theme: string; notifications: boolean }
}
Output
{
  "user": {
    "name": "Alice",
    "age": 30
  },
  "settings": {
    "theme": "dark",
    "notifications": true,
    "language": "no"
  }
}