DEV Community

Cover image for Zod for TypeScript Schema Validation: A Comprehensive Guide
Emi Roberti
Emi Roberti

Posted on

Zod for TypeScript Schema Validation: A Comprehensive Guide

I have really enjoyed using TypeScript in all my node projects, and I like enforcing static type safety in all my applications. However, static typing doesn’t validate data at runtime, leaving your application vulnerable to unexpected inputs. This is where Zod, a TypeScript-first schema validation library, shines and I started to make use of it on frontend React applications and backend, express, AWS Lambdas applications.

Zod helps validate and parse incoming data while ensuring it adheres to predefined schemas. It’s intuitive, lightweight, and designed specifically for TypeScript users. It can validate API responses, user input, or configurations.

Why Zod?

  • TypeScript-First: Zod generates static TypeScript types directly from schemas.
  • Runtime Validation: Validates data at runtime, ensuring it conforms to your defined schema.
  • Rich Ecosystem: Offers powerful features like object schemas, union types, and transformations.
  • Simplicity: No boilerplate or extra dependencies. Zod is intuitive and easy to integrate.

Getting Started

Installation

You can install Zod via npm or yarn:

npm install zod
# or
yarn add zod
Enter fullscreen mode Exit fullscreen mode

Defining Schemas

With Zod, you define schemas to validate your data. Here’s an example:

import { z } from "zod";

const userSchema = z.object({
  name: z.string(),
  age: z.number().int().positive(),
  email: z.string().email(),
  isAdmin: z.boolean().optional(), // Optional field
});

// Sample data
const userData = {
  name: "John Doe",
  age: 30,
  email: "john.doe@example.com",
};

// Validate data
const parsedData = userSchema.parse(userData);
console.log(parsedData);
Enter fullscreen mode Exit fullscreen mode

In this example:

  • The z.object method defines an object schema.
  • Fields like name and age are validated for their types and constraints.
  • The .parse() method validates the userData object and throws an error if it doesn’t match the schema.

Error Handling

Zod provides detailed error messages when validation fails. Here’s an example:

try {
  userSchema.parse({
    name: "John Doe",
    age: -5, // Invalid age
    email: "invalid-email", // Invalid email
  });
} catch (e) {
  console.error(e.errors);
}
Enter fullscreen mode Exit fullscreen mode

Output:

[
  { "code": "too_small", "minimum": 1, "type": "number", "message": "Value should be greater than 0", "path": ["age"] },
  { "code": "invalid_string", "validation": "email", "message": "Invalid email", "path": ["email"] }
]
Enter fullscreen mode Exit fullscreen mode

Using TypeScript Types

Zod schemas automatically infer TypeScript types. Use the z.infer, this is really helpful once the schema has been defined. utility to extract types:

type User = z.infer<typeof userSchema>;

const user: User = {
  name: "Jane Doe",
  age: 25,
  email: "jane.doe@example.com",
};
Enter fullscreen mode Exit fullscreen mode

Advanced Features

Transformations

Zod can transform input data while validating it:

const stringToNumberSchema = z.string().transform((str) => parseInt(str));

const result = stringToNumberSchema.parse("42");
console.log(result); // 42 (as a number)
Enter fullscreen mode Exit fullscreen mode

Union Types

Define schemas that accept multiple valid types using z.union:

const statusSchema = z.union([z.literal("success"), z.literal("failure")]);

statusSchema.parse("success"); // Passes
statusSchema.parse("unknown"); // Throws an error
Enter fullscreen mode Exit fullscreen mode

Array Validation

Zod can validate arrays of items:

const numberArraySchema = z.array(z.number());

numberArraySchema.parse([1, 2, 3]); // Passes
numberArraySchema.parse(["1", "2", "3"]); // Throws an error
Enter fullscreen mode Exit fullscreen mode

Nested Objects

You can validate deeply nested objects using z.object:

const nestedSchema = z.object({
  user: z.object({
    name: z.string(),
    address: z.object({
      street: z.string(),
      city: z.string(),
    }),
  }),
});

nestedSchema.parse({
  user: {
    name: "Alice",
    address: {
      street: "123 Main St",
      city: "Wonderland",
    },
  },
}); // Passes
Enter fullscreen mode Exit fullscreen mode

Custom Validation

Create custom validation logic using .refine:

const passwordSchema = z
  .string()
  .min(8)
  .refine((val) => /[A-Z]/.test(val), { message: "Must contain an uppercase letter" })
  .refine((val) => /[0-9]/.test(val), { message: "Must contain a number" });

passwordSchema.parse("Password1"); // Passes
passwordSchema.parse("password"); // Throws an error
Enter fullscreen mode Exit fullscreen mode

Integration Examples

With Express.js

Validate request data in an Express middleware:

import express from "express";
import { z } from "zod";

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

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

app.post("/users", (req, res) => {
  try {
    const userData = createUserSchema.parse(req.body);
    res.status(200).send(userData);
  } catch (e) {
    res.status(400).send(e.errors);
  }
});

app.listen(3000, () => {
  console.log("Server is running on port 3000");
});
Enter fullscreen mode Exit fullscreen mode

With React

Validate form data in a React component:

import React, { useState } from "react";
import { z } from "zod";

const formSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

function App() {
  const [formData, setFormData] = useState({ name: "", email: "" });
  const [errors, setErrors] = useState<string | null>(null);

  const handleSubmit = () => {
    try {
      formSchema.parse(formData);
      alert("Form submitted successfully!");
    } catch (e) {
      setErrors(e.errors);
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Name"
        value={formData.name}
        onChange={(e) => setFormData({ ...formData, name: e.target.value })}
      />
      <input
        type="email"
        placeholder="Email"
        value={formData.email}
        onChange={(e) => setFormData({ ...formData, email: e.target.value })}
      />
      <button onClick={handleSubmit}>Submit</button>
      {errors && <pre>{JSON.stringify(errors, null, 2)}</pre>}
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

I hope you got some useful information from this article, I have been using Zod in my code base and its intuitive, and TypeScript-first library for schema validation and runtime type checking.

Whether you’re building APIs, processing user input, or working on complex configurations, Zod provides a way to ensure your data conforms to expectations.

Its ability to infer TypeScript types, handle transformations, and offer detailed error messages makes Zod a must-have tool for any TypeScript project. To have more professional code base this library can eliminate many common runtime errors that are hard to catch when the applications get very large, and also if you are working in teams its a good way to make sure you have some validation standards and policies in your code base

I have included sample project as well on github:

https://github.com/EmiRoberti77/zod_ts_validation

Emi Roberti - Happy Coding

Top comments (0)