Zod Showcase

Core Basics

The fundamental building blocks of Zod. Every schema you write will use some combination of these primitives.

Primitive Types

Zod validates each field against its declared type. Try changing age to a string or removing active to see the errors.

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
}

Nested Objects

Zod validates nested objects recursively. Try setting email 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. Try adding a number to the tags array.

Schema
import { z } from 'zod';

const schema = z.object({
  tags: z.array(z.string()),
  scores: z.array(z.number()),
});
Inferred Type
type Schema = {
  tags: string[];
  scores: number[];
}
Output
{
  "tags": [
    "svelte",
    "zod"
  ],
  "scores": [
    95,
    87,
    100
  ]
}

Optional and Nullable

.optional() means the field may be absent entirely. .nullable() means the field must be present but can be null. They are not the same. Try removing nickname vs setting it to null.

Schema
import { z } from 'zod';

const schema = z.object({
  name: z.string(),
  // May be absent from the object
  nickname: z.string().optional(),
  // Must be present, but can be null
  avatar: z.string().nullable(),
});
Inferred Type
type Schema = {
  name: string;
  nickname?: string | undefined;
  avatar: string | null;
}
Output
{
  "name": "Alice",
  "avatar": null
}

Multi-value Literals

In Zod v4, z.literal() accepts an array of values. No need to wrap each value in its own z.literal() and union them together. The schema accepts any of the listed values exactly and rejects anything else. Try changing role to superuser.

Schema
import { z } from 'zod';

// ✅ Zod v4: pass an array directly
const schema = z.object({
  role: z.literal(['admin', 'user', 'guest']),
});

// Equivalent to the old Zod v3 pattern:
// z.union([z.literal('admin'), z.literal('user'), z.literal('guest')])
Inferred Type
type Schema = {
  role: "admin" | "user" | "guest";
}
Output
{
  "role": "admin"
}

Strict and Loose Objects

By default, z.object() silently strips unknown keys. z.strictObject() rejects extra keys with an error, which is useful when you want to catch unexpected fields. z.looseObject() passes extra keys through unchanged. Both replace the deprecated .strict() and .passthrough() methods from Zod v3. The default input includes an extra role key. Try removing it to see the strict schema pass.

Schema
import { z } from 'zod';

// ❌ Extra keys are rejected (v4 replaces .strict())
const StrictSchema = z.strictObject({
  name: z.string(),
  age: z.number(),
});

// ✅ Extra keys are passed through (v4 replaces .passthrough())
const LooseSchema = z.looseObject({
  name: z.string(),
  age: z.number(),
});

// Default z.object() silently strips unknown keys
Inferred Type
// StrictSchema — fails if extra keys present:
type Strict = { name: string; age: number }

// LooseSchema — extra keys pass through:
type Loose = { name: string; age: number }
// (extra keys appear in output but aren't typed)
Output
  • (root) Unrecognized key: "role"