Schema validation in TypeScript with Zod

Schema validation in TypeScript with Zod

Photo by Ben White on Unsplash

In the wild world of software development, data integrity is king. Without it, applications crumble under the weight of unexpected or malformed information. This is where Zod, a powerful data validation library for TypeScript, steps in to be your knight in shining armour.

What is Zod?

Zod is a library for creating, parsing, and validating data structures in TypeScript. It's designed to provide a seamless way to enforce type safety at runtime, bridging the gap between the static type checks provided by TypeScript and the dynamic nature of user input and external data sources.

Why Do We Need Zod?

TypeScript is a powerful tool for adding static type checking to your Typescript code, helping you catch errors at compile time. However, TypeScript's type checking is limited to the code you write and compile; it doesn't extend to data that your application receives at runtime, such as:

  • User Input: Data entered by users through forms or other input elements.

  • API Responses: Data received from external APIs, which might not always conform to the expected structure.

  • External Data Sources: Data from databases, file systems, or other external sources that your application interacts with.

This is where Zod comes into play. Zod allows you to define schemas that describe the expected structure and content of your data. When you validate data against a Zod schema, Zod checks that the data matches the schema at runtime and either returns the validated data or throws an error if the data doesn't conform to the schema.

Here's a simple example to illustrate this:

import { z } from 'zod';

// Define a schema for a user object
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

// Simulate data received from an API
const userData = {
  id: 1,
  name: 'John Doe',
  email: 'johndoe@example.com',
};

// Validate the data against the schema
try {
  const user = UserSchema.parse(userData);
  console.log('Valid user data:', user);
} catch (error) {
  console.error('Invalid user data:', error.errors);
}

In this example, UserSchema defines the expected structure for a user object, including the types of the id, name, and email fields. The parse method is used to validate the userData object against the schema. If userData conforms to the schema, it's returned as a validated user object. If not, an error is thrown with details about the validation failures.

Example of Using Zod in a General Typescript Project

import { z } from 'zod';

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

const validateUser = (userData: any) => {
  try {
    UserSchema.parse(userData);
    console.log('User data is valid!');
  } catch (error) {
    console.error('Invalid user data:', error.errors);
  }
};

const userData = {
  name: 'Jane Doe',
  age: 28,
  email: 'jane.doe@example.com',
};

validateUser(userData);

Using Zod in tRPC Projects

In tRPC projects, Zod can be used to define the input and output schemas for your API procedures, ensuring that the data exchanged between the client and server is valid and correctly typed.

Here's an example of how you might use Zod in a tRPC project:

import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';

export const itemRouter = createTRPCRouter({
    get: protectedProcedure
        .input(
            z.object({
                id: z.number(),
            }),
        )
        .query(({ input, ctx }) => {
            return ctx.prisma.item.findUnique({
                where: {
                    id: input.id,
                },
            });
        }),
});

In this example, the getUser query in the tRPC router uses a Zod schema to validate the input, ensuring that the userId is a string. This adds an extra layer of type safety to your tRPC API, making it more robust and reliable.

Integration with Other Libraries

Zod can be seamlessly integrated with various libraries and frameworks, enhancing its versatility. For instance, you can use Zod with:

  • Form Libraries: Integrate Zod with form libraries like React Hook Form or Formik for form validation.

  • API Libraries: Use Zod with libraries like Axios or Fetch for validating API responses.

Advanced Schema Composition

Zod supports advanced schema composition techniques, such as:

  • Intersection: Combine multiple schemas into one, requiring data to satisfy all schemas.

  • Union: Create a schema that allows data to match any one of multiple schemas.

  • Discriminated Unions: Handle complex scenarios where you need to choose between multiple schemas based on a discriminator field.

Custom Error Messages

Zod allows you to customize error messages for validation failures, making it easier to provide user-friendly feedback:

const UserSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  age: z.number().positive('Age must be positive'),
});

Asynchronous Validation

Zod supports asynchronous validation, which is useful when you need to perform validation that involves asynchronous operations, such as checking the uniqueness of a username in a database:

const UsernameSchema = z.string().superRefine(async (username, ctx) => {
  const exists = await checkUsernameExists(username);
  if (exists) {
    ctx.addIssue({
      code: 'custom',
      message: 'Username already exists',
    });
  }
});

Parsing and Type Inference

Zod not only validates data but also safely parses it, ensuring that the resulting data is correctly typed. This eliminates the need for manual type assertions after validation.

Extensibility

Zod is designed to be extensible, allowing you to create custom validation functions and even extend the library with your schema types if needed.

Active Development and Community Support

Zod is actively developed and maintained, with a growing community of users. This means that the library is continually improving, and there is a good chance of getting support and finding resources to help you get started or solve any issues you encounter.

Pros of Zod

  1. Type Safety: Zod enhances type safety by validating data structures at runtime, reducing the likelihood of type-related bugs.

  2. Type Inference: Zod schemas automatically infer TypeScript types, reducing redundancy and keeping your codebase DRY (Don't Repeat Yourself).

  3. Customizable Validation: Zod allows for custom validation logic, enabling you to handle complex validation scenarios.

  4. Error Handling: Zod provides detailed error messages, making it easier to identify and fix validation issues.

Cons of Zod

  1. Learning Curve: If you're new to TypeScript or schema validation, there might be a learning curve to using Zod effectively.

  2. Performance Overhead: Like any validation library, Zod introduces some performance overhead, which might be noticeable in performance-critical applications.

Conclusion

Zod is a powerful tool for adding runtime validation to your TypeScript projects, complementing the static type checking provided by TypeScript. It's especially useful in scenarios where you need to validate external data, such as user input or API responses. By integrating Zod into your tRPC projects, you can further enhance the type safety and reliability of your APIs. Whether you're working on a small personal project or a large-scale enterprise application, Zod can help you write more secure, maintainable, and bug-free code.