Getting Started with Zod Validation

Learn how to use Zod for schema validation in your AI testing workflows.

What Is Zod?

Zod is a TypeScript-first schema validation library.

It lets you:

  • Define the shape of data using schemas
  • Validate runtime data against those schemas
  • Infer TypeScript types automatically from the schema
  • Parse and transform input safely

It is commonly used in:

  • Node.js backends
  • React apps
  • API validation layers
  • Form validation
  • AI/LLM structured output validation

Why Zod Is Popular

  • Type inference built in
  • No separate validation and type definition
  • Clean error formatting
  • Works well with React, Next.js, Node
  • Ideal for validating AI structured outputs

Getting Started with Zod

Step 1: Install Zod

npm install zod

or

yarn add zod

Step 2: Create Your First Schema

Create a file:

import { z } from "zod";

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

This defines a schema where:

  • name must be a string
  • age must be a number

Step 3: Validate Data

Option A: parse()

Throws an error if validation fails.

const data = {
  name: "John",
  age: 30,
};

const user = UserSchema.parse(data);
console.log(user);

If invalid:

UserSchema.parse({ name: "John" });

It throws a detailed validation error.


Option B: safeParse()

Does not throw. Returns success status.

const result = UserSchema.safeParse({
  name: "John",
  age: "30",
});

if (!result.success) {
  console.log(result.error.format());
} else {
  console.log(result.data);
}

Recommended for APIs and user input.


Step 4: Add Validation Rules

You can add constraints.

const UserSchema = z.object({
  name: z.string().min(2).max(50),
  age: z.number().int().positive(),
  email: z.string().email(),
});

Examples:

  • .min()
  • .max()
  • .email()
  • .url()
  • .uuid()
  • .regex()

Step 5: Make Fields Optional or Nullable

const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
  bio: z.string().nullable(),
});
  • optional() means the field can be undefined
  • nullable() means it can be null

Step 6: Infer TypeScript Types

This is one of Zod’s biggest advantages.

type User = z.infer<typeof UserSchema>;

Now TypeScript automatically generates:

type User = {
  name: string;
  age?: number;
  bio: string | null;
};

No duplicate interface required.


Step 7: Default Values

const UserSchema = z.object({
  role: z.string().default("user"),
});

If role is missing, it defaults to "user".


Step 8: Transform Data

You can modify values during parsing.

const UserSchema = z.object({
  name: z.string().transform((val) => val.toUpperCase()),
});

Input:

{ "name": "Doe" }

Output:

{ "name": "DOE" }

Step 9: Nested Objects

const AddressSchema = z.object({
  city: z.string(),
  country: z.string(),
});

const UserSchema = z.object({
  name: z.string(),
  address: AddressSchema,
});

Step 10: Arrays

const UsersSchema = z.array(UserSchema);

Or:

const UserSchema = z.object({
  name: z.string(),
  skills: z.array(z.string()),
});

Step 11: Enums

const RoleSchema = z.enum(["admin", "user", "guest"]);

Step 12: Discriminated Unions

Very useful for API responses.

const SuccessResponse = z.object({
  status: z.literal("success"),
  data: z.string(),
});

const ErrorResponse = z.object({
  status: z.literal("error"),
  message: z.string(),
});

const ApiResponse = z.discriminatedUnion("status", [
  SuccessResponse,
  ErrorResponse,
]);

Step 13: Custom Validation

const PasswordSchema = z.string().refine(
  (val) => val.length >= 8,
  { message: "Password must be at least 8 characters" }
);

Example: Validating an Express API Request

app.post("/users", (req, res) => {
  const result = UserSchema.safeParse(req.body);

  if (!result.success) {
    return res.status(400).json(result.error.flatten());
  }

  const user = result.data;
  res.json(user);
});

This prevents:

  • Invalid payloads
  • Type mismatches
  • Unexpected fields