All posts

Intermediate Zod: Validation Workflows and Techniques

14 min read

In the previous article, Beginner Zod: Introduction to Schema Validation, we explored the basics of using Zod, a TypeScript-first schema declaration and validation library, to validate simple data structures.

We covered foundational concepts such as defining basic schemas, validating primitive types, and handling errors. Now that you have a solid understanding of the essentials, it's time to look into more sophisticated validation workflows and techniques such as complex object schemas, nested validations, custom rules, transformations, and more.

Custom Validation Rules

Sometimes, the built-in validation capabilities of Zod aren't enough to capture your application's unique requirements. This is where custom validation rules come into play. Zod allows you to define custom rules using the refine method, enabling you to enforce specific business logic constraints.

Example

For example, let's say we want to validate a password that must contain at least one uppercase letter. We can define a schema that looks like this:

ts
const passwordSchema = z .string() .min(8) .refine((val) => /[A-Z]/.test(val), { message: "Password must contain at least one uppercase letter", });

Demo

Coercion

The coerce feature in Zod provides a powerful way to automatically convert input values to the desired type during validation. This is particularly useful when dealing with form data or API inputs where values might come in as strings but need to be validated as numbers, dates, or booleans.

Example

Here are some common use cases for coercion:

ts
// Number coercion const numberSchema = z.coerce.number(); numberSchema.parse("123"); // Returns 123 (number) numberSchema.parse(true); // Returns 1 numberSchema.parse(false); // Returns 0

Demo

Transformations

Transformations in Zod allow you to modify data during the validation process. This feature is especially useful for normalizing input data, such as converting strings to dates or trimming whitespace from user input. Transformations ensure that your data is in the expected format before it reaches your application logic.

Example

Here are some common use cases for transformations:

ts
// Transform email to lowercase and trim whitespace const emailSchema = z .string() .email() .transform((email) => email.trim().toLowerCase()); // Example usage emailSchema.parse(" User@Example.com "); // Returns "user@example.com"

Demo

warning

You'll notice on this example that extra spaces are invalid for the email() rule. So we'd need the preprocessing to handle that.

Complex Object Schemas

When dealing with intricate data models, complex object schemas are indispensable. These schemas allow you to validate data structures that include nested objects and arrays, ensuring that your application's data is both accurate and consistent. With Zod, you can define these schemas in a way that mirrors your application's data models.

Example

For example, let's say we want to validate a user object that includes an address and a list of roles. We can define a schema that looks like this:

ts
import { z } from "zod"; const userSchema = z.object({ id: z.string().uuid(), name: z.string().min(1), email: z.string().email(), address: z.object({ street: z.string(), city: z.string(), postalCode: z.string().regex(/^[A-Z]\d[A-Z] \d[A-Z]\d$/), // Canadian postal code format }), roles: z.array(z.enum(["admin", "user", "guest"])), });

This schema defines a user object with an id, name, email, and address. The address is itself an object with street, city, and postalCode properties. The roles property is an array of strings, each representing a role that the user has.

To validate a user object, you can use the userSchema.parse() method. This method will validate the user object and return the validated object if it is valid. If the object is not valid, the method will throw an error.

Demo

Address

Combining and Extending Schemas

Zod's combining and extending capabilities allow you to build modular and reusable schema components. This is particularly beneficial when you have shared validation logic across different parts of your application. By extending schemas, you can create new schemas that inherit properties from existing ones, promoting code reuse and consistency.

Example

ts
// Base schema for contact information const contactSchema = z.object({ email: z.string().email(), phone: z.string().regex(/^\+?[1-9]\d{1,14}$/), // E.164 format }); // Extended user schema that includes contact and address const userSchema = z .object({ id: z.string().uuid(), name: z.string().min(1), }) .extend({ contact: contactSchema, }); // Example usage const user = { id: "123e4567-e89b-12d3-a456-426614174000", name: "John Doe", contact: { email: "john@example.com", phone: "+1234567890", }, }; userSchema.parse(user); // Returns validated user object

Demo

Contact

As we've seen, Zod offers a wealth of features for building validation workflows that are both powerful and flexible. By mastering these techniques, you're now equipped to handle more complex validation scenarios.

In the next and final article of our series, "Advanced Zod: Designing Complex Validation Schemas," we'll explore even more sophisticated strategies, including conditional validations, dynamic schema generation, and performance optimization techniques. Stay tuned for more Zod magic!


  • Zod
  • Schema Validation
  • TypeScript
  • Intermediate
Made with ❤️ in 🇨🇦 · Copyright © 2025 Valentin Prugnaud
Foxy seeing you here! Let's chat!
Logo