DEV Community

Cover image for Zod: The Ultimate Validation Library for TypeScript
Ashik
Ashik

Posted on

Zod: The Ultimate Validation Library for TypeScript

This post was originally posted here on my site codespoetry.com

Zod is a TypeScript-first schema validation library. Unlike static type checking, Zod helps you to validate data dynamically at runtime. Whether user input, API response, or any data you want to validate, structure custom errors, Zod is the ultimate tool.

I’ll go through the discussion on schema structure, custom validation, asynchronous checks, and demonstrate how to integrate Zod into a React/Next.js project for form validation, state management, and prop type validation.

Zod Use Cases

1. Schema Definitions

Defining a complex schema using Zod is fun. Array, Object, nested data, nothing is as complex as we may think. Imagine a address schema:

import {z} from 'zod';

const addressSchema = z.object({
  area: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}$/, "Invalid zip code"),  
  country: z.string(),
});
Enter fullscreen mode Exit fullscreen mode

This schema will validate the address and ensure type safety. We can also set custom errors on validation.

import {z} from 'zod';

const addressSchema = z.object({
  area: z.string({ required_error: "Area is required" }),
  city: z.string({ required_error: "City is required" }),
  zipCode: z.string({ required_error: "Zip code is required" }).regex(/^\d{5}$/, "Invalid     zip code"),  
  country: z.string({ required_error: "Country is required" }),
});
Enter fullscreen mode Exit fullscreen mode

This schema will validate the data types and return an error if any field is missing or empty. We can also chain the validation methods. And here are a few examples

// validate both undefined and empty fields or data 
z.string().nonempty()

// validate undefined, nonempty, minimum characters
z.string().nonempty().min(100);

// validate undefined, nonempty, minimum and max characters
z.string().nonempty().min(100).max(500);

// setting up custom error message
z.string({ required_error: "String is required" })
.nonempty({message: "String can't be empty"})
.min(100, {message: "A minimum of 100 characters is required"})
.max(500, {message: "String can't be more than 500 characters"}); 
Enter fullscreen mode Exit fullscreen mode

Note that in the validation chain, if multiple validations fail, the last error message will be assigned to the error data. For example, for a blank text field, both ‘nonempty’ and ‘min’ are correct. And the error defined in the min() method will be added to the error data. This is how Zod prioritizes errors when multiple rules fail.

2. Advance validations

All the default validation methods can be used in different contexts. Like, you can use the ‘min()’ method in array data to ensure a minimum of data is present. Get back to the address schema

import {z} from 'zod';

const addressSchema = z.array(
z.object({
  area: z.string({ required_error: "Area is required" }),
  city: z.string({ required_error: "City is required" }),
zipCode: z.string({ required_error: "Zip code is required" }).regex(/^\d{5}$/, "Invalid zip code"),  
country: z.string({ required_error: "Country is required" }),
})
).min(1, {message: "A minimum of one address is required"});
Enter fullscreen mode Exit fullscreen mode

This will prevent form submission if there is no address in the submitted data. The address object will be in an array, as this is wrapped in Zod array data, to clarify. We can also use other methods to validate our data or a collection of data.

3. Custom Validations

In addition to the default validation methods, our project may require custom validations. Calculating age based on DOB input, comparing dates, validating a field’s data that depends on a different field, etc. Zod has refine and superrefine methods to implement custom validation. The refine is a syntactic sugar of superrefine. Here is how we can use refine:

jobDescription: z.string().refine((value) => {
         if (value.trim().split(" ").length > 500) {
           return false;
         }
         return true;
         },
         { message: "Job description can't be more than 500 words" }
       ).optional(),

Enter fullscreen mode Exit fullscreen mode

The above Zod schema checks if the job description words are longer than 500 words. If it is, it will return false. However, it's an optional data as we added Zod's optional() method.

The refine method should always return a boolean. If it’s untrue, the validation fails, and the error state updates with the message passed as the second argument to the refine function. To clarify the entire scenario, the refine method takes two arguments. The first one is a validation function. You can check the value and run your refinement as you want to validate your data. The second argument should be an Object containing a message for the error state. The default Zod’s max() method checks characters. Our goal was to validate words. So, we used refine here.

As I discussed in “Advance Validations” how we can chain validation methods, we can chain the refine() method as well and validate our data with different conditions. We can also run the refine() method on a whole object data group or on the array. For a data group, we’ll get all the data in the value parameter. Here is an example.

z
  .array(
    z
      .object({
        company: z
          .string({ required_error: "Comapny name is required" })
          .nonempty({ message: "Company name can't be empty" }),
        jobTitle: z
          .string({ required_error: "Job title is required" })
          .nonempty({
            message: "Job title can't be empty",
          }),
        startingDate: z.date({ required_error: "Starting date is required" }),
        endDate: z
          .date({ required_error: "End date is required" })
          .optional()
          .nullable(),
        currentlyWorking: z.boolean().optional(),
        jobDescription: z
          .string()
          .refine(
            (value) => {
              if (value.trim().split(" ").length > 500) {
                return false;
              }
              return true;
            },
            { message: "Job description can't be more than 500 words" }
          )
          .optional(),
      })
      .refine(
        (date) => {
          if (date.currentlyWorking) return true;
          return compareDesc(date.startingDate, date.endDate ?? new Date()) ==
            0 ||
            compareDesc(date.startingDate, date.endDate ?? new Date()) == -1
            ? false
            : true;
        },
        {
          message:
            "Star and End date can't be the same or End date is before the Start date",
          path: ["endDate"],
        }
      )
  )
  .optional()
  .default([]);
Enter fullscreen mode Exit fullscreen mode

The compareDesc function is from date-fns to check date equality.

We can also pass an async function as our validator and validate data.

const stringSchema = z.string().refine(async (val) => val.length <= 8);

Enter fullscreen mode Exit fullscreen mode

Zod’s parse and parseAsync are the methods to parse a Zod schema and check if the data is valid. The parseAsync method is used to validate data asynchronously for an asynchronous validation function passed to the refine method.

const stringSchema = z.string();

stringSchema.parse("fish"); // => returns "fish"
stringSchema.parse(12); // throws error
Enter fullscreen mode Exit fullscreen mode

The parse and parseAsync throw an error on failing validation. If we want to get an object containing parsed data or an error, we can use safeParse and safeParseAsync.

Integration Zod into a React Project

1. Form validation with React Hook Form

Zod pairs beautifully with form libraries like React Hook Form for validating user inputs. Here’s a login form example:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

function LoginForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schema),
  });

  const onSubmit = (data) => {
    console.log("Valid data:", data);
  };

  return (
    <div>
      <input {...register("email")} placeholder="Email" />
      {errors.email && <p>{errors.email.message}</p>}
      <input type="password" {...register("password")} placeholder="Password" />
      {errors.password && <p>{errors.password.message}</p>}
      <button onClick={handleSubmit(onSubmit)}>Login</button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Zod validates the form data, and React Hook Form manages the UI state, displaying errors as needed.

2. Prop Types Validation

const propsSchema = z.object({
  title: z.string(),
  count: z.number().int().positive(),
});

function MyComponent(props) {
  const validatedProps = propsSchema.parse(props);
  return <div>{validatedProps.title}: {validatedProps.count}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Zod provides detailed errors if props don’t match the schema, enhancing debugging.

Zod Documentations:

Zod has its rich documentation on use cases. You can check it out here and dive deep.

Conclusion

Zod is a handy, small yet powerful tool to enhance your development skills. Zod provides clear error messages and a straightforward API. This makes it an excellent choice for TypeScript applications needing robust data validation.

If you like this article, feel free to share it in your network and comment below with any feedback or suggestions.

Top comments (0)