10 Real-World Zod Validation Examples

Sharukhan PatanSharukhan Patan
8 min read

Zod is a lightweight, TypeScript-first library for validating data at runtime. It lets you define schemas that ensure your data is exactly what you expect.

Runtime validation is important because TypeScript types are removed after the build. You still need to verify inputs from users, APIs, or external sources.

This guide walks you through 10 real-world Zod examples with code. You'll see how to use it in forms, APIs, and common dev tasks.

1. Zod Validation for API Response

When you fetch data from an API—REST or tRPC—it's never 100% safe to trust the shape of the response.
Zod validation for API response ensures the data matches the expected structure before your app uses it.

Here’s how to validate an API response using safeParse() and handle missing fields gracefully:

import { z } from "zod";

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email().optional(), // optional field
});

async function getUser() {
  const res = await fetch("/api/user");
  const data = await res.json();

  const result = userSchema.safeParse(data);

  if (!result.success) {
    console.error("Invalid API response", result.error.format());
    return null; // or fallback logic
  }

  return result.data;
}

In this example, safeParse() prevents your app from crashing if the response is missing required fields or has the wrong types.
Use this pattern to make your API integrations more robust and bug-resistant.

2. Validate JSON Payload with Zod

When handling a POST request, you need to validate JSON payload with Zod to avoid bad or unexpected data.
This is especially useful in frameworks like Express.js or Next.js API routes.

Here’s a simple example using Zod to validate and sanitize the body of a POST request:

// For Express.js
import { z } from "zod";
import express from "express";

const app = express();
app.use(express.json());

const payloadSchema = z.object({
  title: z.string().min(1),
  content: z.string(),
  tags: z.array(z.string()).optional(),
});

app.post("/blog", (req, res) => {
  const parsed = payloadSchema.safeParse(req.body);

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

  const cleanData = parsed.data;
  // Proceed with safe, validated data
  res.status(200).json({ message: "Blog created", data: cleanData });
});

The same approach works in Next.js API routes by using req.body and running it through safeParse().
Zod helps you validate and sanitize inputs, making your endpoints cleaner and safer.

3. Zod Example for Nested Form Data

Complex forms often involve nested structures like profile info, address, and user preferences.
Here’s a practical Zod example for nested form data using deeply nested schemas.

import { z } from "zod";

const formSchema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    zip: z.string().length(5),
  }),
  preferences: z
    .object({
      newsletter: z.boolean(),
      notifications: z.boolean(),
    })
    .partial(), // Make all preferences optional
});

// Example form data
const formData = {
  name: "Jane Doe",
  email: "jane@example.com",
  address: {
    street: "123 Main St",
    city: "New York",
    zip: "10001",
  },
  preferences: {}, // This is fine due to .partial()
};

const result = formSchema.safeParse(formData);

if (!result.success) {
  console.log("Validation failed:", result.error.format());
} else {
  console.log("Validated data:", result.data);
}

Zod makes it easy to validate nested form data, even when parts of it are optional.
Use .partial() to mark an entire nested object as optional without repeating yourself.

4. Validate Date Strings with Zod

Working with dates from forms or APIs? It’s important to validate date strings with Zod to avoid invalid or broken formats.
You can accept YYYY-MM-DD or ISO strings and convert them into real Date objects safely.

Here’s how to do it using z.string().refine() or z.string().transform():

import { z } from "zod";

// Accepts YYYY-MM-DD and transforms to Date object
const dateSchema = z
  .string()
  .refine((val) => !isNaN(Date.parse(val)), {
    message: "Invalid date format. Expected YYYY-MM-DD or ISO string.",
  })
  .transform((val) => new Date(val));

// Example usage
const input = { dob: "2024-10-05" };

const schema = z.object({
  dob: dateSchema,
});

const result = schema.safeParse(input);

if (!result.success) {
  console.log("Date validation failed:", result.error.format());
} else {
  console.log("Parsed date object:", result.data.dob); // Date instance
}

This approach ensures your app accepts only valid date strings and cleanly converts them to JavaScript Date objects.
Use this when dealing with user birthdates, event dates, or any time-sensitive data.

5. Validate Enum Values with Zod

When you want to restrict values to a set list, it’s best to validate enum values with Zod style using z.enum() or z.nativeEnum().
This helps ensure fields like user roles only accept specific options like "admin", "user", or "guest".

Here’s a simple example with a user role enum:

import { z } from "zod";

// Using z.enum with string literals
const roleSchema = z.enum(["admin", "user", "guest"]);

// Or using TypeScript enum and z.nativeEnum
enum UserRole {
  Admin = "admin",
  User = "user",
  Guest = "guest",
}
const nativeRoleSchema = z.nativeEnum(UserRole); // deprecated

// Example validation
const userSchema = z.object({
  name: z.string(),
  role: roleSchema, // or nativeRoleSchema
});

const input = { name: "Alice", role: "admin" };

const result = userSchema.safeParse(input);

if (!result.success) {
  console.log("Invalid role:", result.error.format());
} else {
  console.log("Valid user:", result.data);
}

You can reuse enums both as TypeScript types and Zod schemas for consistent validation and type safety.
This pattern keeps your code clean and avoids invalid values sneaking in.

6. Zod Schema for Login Form

Creating a login form? Use a Zod schema for login form to validate both email and password securely.
You can enforce email format and password strength with .min() and .regex() checks.

Here’s a reusable schema for front-end and back-end validation:

import { z } from "zod";

const loginSchema = z.object({
  email: z.string().email({ message: "Invalid email address" }),
  password: z
    .string()
    .min(8, { message: "Password must be at least 8 characters" })
    .regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" })
    .regex(/[0-9]/, { message: "Password must contain at least one number" }),
});

// Example usage (frontend or backend)
const loginData = {
  email: "user@example.com",
  password: "Passw0rd",
};

const result = loginSchema.safeParse(loginData);

if (!result.success) {
  console.log("Validation errors:", result.error.format());
} else {
  console.log("Valid login data:", result.data);
}

Using the same schema on both client and server means consistent validation and fewer bugs.
Zod keeps your login form secure and user-friendly.

7. Validate Phone Number in Zod

Need to validate phone number in Zod for forms or APIs? You can use regex inside z.string().refine() to check formats.
This lets you support multiple country formats or enforce strict E.164 formatting.

Here’s an example validating E.164 phone numbers on both client and server:

import { z } from "zod";

const phoneSchema = z
  .string()
  .refine((val) => /^\+?[1-9]\d{1,14}$/.test(val), {
    message: "Invalid phone number format. Use E.164 format like +1234567890.",
  });

// Example schema for a contact form
const contactSchema = z.object({
  name: z.string().min(1),
  phone: phoneSchema,
});

// Example input
const input = {
  name: "Sharukhan Patan",
  phone: "+14155552671",
};

const result = contactSchema.safeParse(input);

if (!result.success) {
  console.log("Phone validation failed:", result.error.format());
} else {
  console.log("Valid contact info:", result.data);
}

This way, you can ensure phone numbers are valid and consistent across your app.
Use this pattern for reliable client + server form validation with Zod.

8. Zod Schema for Pagination Params

Handling pagination? Use a Zod schema for pagination params to validate query parameters like page, limit, sort, and filter.
Coerce strings to numbers, ensure integers, and add defaults for smoother API handling.

Here’s a robust example:

import { z } from "zod";

const paginationSchema = z.object({
  page: z.coerce.number().int().min(1).default(1),
  limit: z.coerce.number().int().min(1).max(100).default(10),
  sort: z.string().optional(), // e.g., "name" or "-date"
  filter: z.string().optional(),
});

// Example usage with query params (e.g., Express or Next.js)
const queryParams = {
  page: "2",
  limit: "20",
  sort: "-createdAt",
};

const result = paginationSchema.safeParse(queryParams);

if (!result.success) {
  console.log("Invalid pagination params:", result.error.format());
} else {
  console.log("Validated pagination params:", result.data);
  // data.page and data.limit are numbers now
}

With .coerce(), you can handle string inputs gracefully (like from URL queries) and still enforce proper types.
Defaults make your API more user-friendly when params are missing.

9. Zod Transform Date from Epoch

Sometimes, APIs send dates as timestamps (epoch). Use Zod transform date from epoch to accept a number and return a JavaScript Date object.
This ensures you work with proper dates in your app instead of raw numbers.

Here’s how to do it with .transform() and safe parsing:

import { z } from "zod";

const epochDateSchema = z
  .number()
  .int()
  .nonnegative()
  .transform((val) => new Date(val * 1000)); // assuming seconds

// Example usage
const input = {
  createdAt: 1686316800, // epoch timestamp in seconds
};

const schema = z.object({
  createdAt: epochDateSchema,
});

const result = schema.safeParse(input);

if (!result.success) {
  console.log("Date parsing failed:", result.error.format());
} else {
  console.log("Parsed Date object:", result.data.createdAt);
}

This approach safely converts epoch timestamps into usable Date objects,
making date handling consistent and error-proof.

10. Zod Schema with Defaults Example

Setting up user profiles? A Zod schema with defaults example helps you provide fallback values for missing fields like strings, numbers, or nested objects.
Combine .default() with .optional() to make your schema flexible and user-friendly.

Check out this example for a user profile setup:

import { z } from "zod";

const profileSchema = z.object({
  username: z.string().default("Anonymous"),
  age: z.number().int().default(18),
  preferences: z
    .object({
      theme: z.string().default("light"),
      notifications: z.boolean().default(true),
    })
    .optional()
    .default({}), // default empty object if preferences missing
});

// Example input missing some fields
const input = {
  age: 25,
};

const result = profileSchema.safeParse(input);

if (!result.success) {
  console.log("Validation errors:", result.error.format());
} else {
  console.log("Validated profile with defaults:", result.data);
  /*
    {
      username: "Anonymous",
      age: 25,
      preferences: { theme: "light", notifications: true }
    }
  */
}

Using defaults keeps your data complete and avoids unnecessary null checks.
It’s perfect for forms where users might skip optional fields.

Conclusion

Zod makes validating forms and API data super simple and reliable.
By defining clear schemas, you catch errors early and keep your app bug-free.

Using shared schemas on both client and server saves time and ensures consistency.
Ready to dive deeper? Check out the official Zod docs or explore our next article for more tips!

0
Subscribe to my newsletter

Read articles from Sharukhan Patan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sharukhan Patan
Sharukhan Patan