SKIP TO MAIN
-- views

How to validate data using Zod

February 4, 2024
import { z } from 'zod';
 
export const FormSchema = z.object({
    id: z.string(),
    customerId: z.string({
        invalid_type_error: 'Please select a customer.',
    }),
    amount: z.coerce
        .number()
        .gt(0, { message: 'Please enter an amount greater than $0.' }),
    status: z.enum(['pending', 'paid'], {
        invalid_type_error: 'Please select an invoice status.',
    }),
    date: z.string(),
});

What is Zod?#

According to the official website, Zod is a TypeScript-first schema validation library with static type inference.

The goal is to eliminate duplicative type declarations. With Zod, you declare a validator once and Zod will automatically infer the static TypeScript type

Some other great aspects:

  • Zero dependencies
  • Works in Node.js and all modern browsers
  • Tiny: 8kb minified + zipped
  • Immutable: methods (e.g. .optional()) return a new instance
  • Concise, chainable interface
  • Functional approach: parse, don't validate
  • Works with plain JavaScript too! You don't need to use TypeScript.

Validator#

Let’s look at defining a basic zod schema

import z from 'zod';
 
const SimpleSchema = z.object({
    name: z.string(),
    age: z.number(),
});

After defining our schema, we use it to validate the shape of any data passed into it. If there are errors, it will throw a ZodError. For a more comprehensive guide on Zod errors and the ZodError type, I recommend reading the documentation.

To safely use our schema, it’s best to wrap it in a try-catch block:

try {
    const results = SimpleSchema.parse(data);
    // do something with the results
} catch (error) {
    console.error(error);
    // apply error handling
}

If you don't want Zod to throw errors when validation fails. use .safeParse. This method returns an object containing either the successfully parsed data or a ZodError instance containing detailed information about the validation problems.

const results = SimpleSchema.safeParse(data);
if (!results.success) {
    console.error(results.error);
    // apply error handling
} else {
    // do something with the result.data
}

Let’s use Zod to validate the form's data

const invalid_type_error = 'Invalid type provided for this field';
const required_error = 'This field cannot be blank';
 
export const SignUpSchema = z.object({
    fullName: z
        .string({ invalid_type_error, required_error })
        .min(1, 'Value is too short'),
    username: z
        .string({ invalid_type_error, required_error })
        .min(1, 'Value is too short'),
    email: z
        .string({ invalid_type_error, required_error })
        .email('Please provide a valid email')
        .min(1, 'Value is too short'),
    password: z
        .string({ invalid_type_error, required_error })
        .min(6, 'Password is too short'),
});

A common issue with Zod is that an empty string is valid as a type string, the fix for this would be specifying a minimum length of 1 and by default, all values are required

Refinements#

Additionally, Zod lets you provide custom validation logic via refinements.

const passwordForm = z
    .object({
        password: z.string(),
        confirm: z.string(),
    })
    .refine((data) => data.password === data.confirm, {
        message: "Passwords don't match",
        path: ["confirm"], // path of error
    });

Type inference#

You can extract the TypeScript type of any schema with z.infer<typeof mySchema>

import { z } from "zod";
 
const User = z.object({
    id: z.string(),
    username: z.string(),
});
 
// Extract the inferred type
type User = z.infer<typeof User>;
// { username: string, id: string }

Real-life example#

The provided code is an example from the Next.js learning resource.

'use client';
 
import { Button } from '@/app/ui/button';
import { createInvoice } from '@/app/lib/actions';
import { useFormState } from 'react-dom';
 
export default function Form() {
    const initialState = { message: null, errors: {} };
    const [state, dispatch] = useFormState(createInvoice, initialState);
 
    return (
        <form action={dispatch}>
            <div>
                <label htmlFor="amount">Choose an amount</label>
                <input
                    id="amount"
                    name="amount"
                    type="number"
                    step="0.01"
                    placeholder="Enter USD amount"
                    aria-describedby="amount-error"
                />
                <div id="amount-error" aria-live="polite" aria-atomic="true">
                    {state?.errors?.amount?.map((error: string) => (
                        <p key={error}>{error}</p>
                    ))}
                </div>
            </div>
            <Button type="submit">Create Invoice</Button>
        </form>
    );
}
  • The useFormState hook is used to manage the form state.

    • The form has an action attribute set to the dispatch function obtained from the useFormState hook.
    • The state object (state) contains messages and errors.
  • There is an input field for entering the invoice amount. It accepts numeric values and allows decimal numbers.

  • The errors are mapped for displaying validation errors related to the amount field.

useFormState Hook is currently only available in React’s Canary and experimental channels. it allows you to update the state based on the result of a form action.

// @/app/lib/actions
import { z } from 'zod';
 
// Define a Zod schema for form validation
const FormSchema = z.object({
    amount: z.coerce
        .number()
        .gt(0, { message: 'Please enter an amount greater than $0.' }),
});
 
export type State = {
    errors?: {
        amount?: string[];
    };
    message?: string | null;
};
 
export async function createInvoice(prevState: State, formData: FormData) {
    // Validate form data against the FormSchema
    const validatedFields = CreateInvoice.safeParse({
        amount: formData.get('amount'),
    });
 
    // If form validation fails, return errors early. Otherwise, continue.
    if (!validatedFields.success) {
        return {
            // Return validation errors and a custom error message
            errors: validatedFields.error.flatten().fieldErrors,
            message: 'Missing Fields. Failed to Create Invoice.',
        };
    }
 
    // Additional logic for preparing data or
    // insertion into the database can be added here
    const { amount } = validatedFields.data;
}

We're defining a React component (Form) for creating invoices. The form has an input field for the invoice amount, validated using a Zod schema (FormSchema). The createInvoice function manages form validation, returns errors if any, and prepares validated data for potential database insertion.

Conclusion#

Zod has become a handy tool for developers dealing with form validation, especially when combined with TypeScript. Using Zod's schema declaration and validation features, developers can ensure that the data entered into forms aligns with the specified structure and adheres to predefined validation rules.

References#

GET IN TOUCH

Let’s work together

I build exceptional and accessible digital experiences for the web

WRITE AN EMAIL