Zod 4 - Evolution of TypeScript Schema Validation

2024.12.24

What is Zod

Zod is a TypeScript-first schema declaration and validation library. Just by defining a schema, you can perform validation and type inference simultaneously.

Zod 4 New Features

Performance Improvements

Validation Speed:
- Zod 3: 100,000 ops/sec
- Zod 4: 300,000 ops/sec (3x faster)

Parse Speed:
- Up to 5x faster with complex schemas

New Metadata API

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().describe('User name'),
  email: z.string().email().describe('Email address'),
  age: z.number().min(0).max(150).describe('Age')
}).describe('User information');

// Get metadata
const metadata = userSchema.description;
const fields = userSchema.shape;

Basic Usage

Schema Definition

import { z } from 'zod';

// Primitive types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();

// Object
const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['user', 'admin']),
  createdAt: z.date().default(() => new Date())
});

// Type inference
type User = z.infer<typeof userSchema>;
// {
//   id: number;
//   name: string;
//   email: string;
//   age?: number;
//   role: 'user' | 'admin';
//   createdAt: Date;
// }

Validation

// parse: Throws exception on failure
const user = userSchema.parse({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
  role: 'user'
});

// safeParse: Returns result as object
const result = userSchema.safeParse(input);
if (result.success) {
  console.log(result.data);
} else {
  console.error(result.error.issues);
}

Transform

const schema = z.string()
  .transform(val => val.toLowerCase())
  .transform(val => val.trim());

const cleanSchema = z.object({
  price: z.string().transform(val => parseFloat(val)),
  quantity: z.string().transform(val => parseInt(val, 10))
});

Advanced Features

Refinement

const passwordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .refine(
    (val) => /[A-Z]/.test(val),
    { message: 'Please include an uppercase letter' }
  )
  .refine(
    (val) => /[0-9]/.test(val),
    { message: 'Please include a number' }
  );

// superRefine (return multiple errors)
const formSchema = z.object({
  password: z.string(),
  confirmPassword: z.string()
}).superRefine((data, ctx) => {
  if (data.password !== data.confirmPassword) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Passwords do not match',
      path: ['confirmPassword']
    });
  }
});

Union and Discriminator

// Regular union
const responseSchema = z.union([
  z.object({ status: z.literal('success'), data: z.any() }),
  z.object({ status: z.literal('error'), message: z.string() })
]);

// Discriminated union (faster)
const eventSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
  z.object({ type: z.literal('scroll'), offset: z.number() }),
  z.object({ type: z.literal('keypress'), key: z.string() })
]);

Recursive Schema

interface Category {
  name: string;
  subcategories: Category[];
}

const categorySchema: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    subcategories: z.array(categorySchema)
  })
);

Partial and Required

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string()
});

// Make all optional
const partialUser = userSchema.partial();

// Make specific fields optional only
const updateUser = userSchema.partial({
  name: true,
  email: true
});

// Make optional fields required
const requiredUser = userSchema.required();

API Validation

Express

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

const createUserSchema = z.object({
  body: z.object({
    name: z.string(),
    email: z.string().email()
  }),
  query: z.object({
    notify: z.string().optional()
  })
});

function validate<T extends z.ZodType>(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req);
    if (!result.success) {
      return res.status(400).json({ errors: result.error.issues });
    }
    next();
  };
}

app.post('/users', validate(createUserSchema), (req, res) => {
  // Validated
});

tRPC

import { z } from 'zod';
import { publicProcedure, router } from './trpc';

export const appRouter = router({
  createUser: publicProcedure
    .input(z.object({
      name: z.string().min(1),
      email: z.string().email()
    }))
    .mutation(async ({ input }) => {
      return db.user.create({ data: input });
    })
});

Integration with Form Libraries

React Hook Form

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

const schema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Please enter a valid email address')
});

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name')} />
      {errors.name && <span>{errors.name.message}</span>}
      {/* ... */}
    </form>
  );
}

Summary

Zod 4 makes TypeScript schema validation even faster and easier to use. With its integration with type inference, rich validation features, and framework compatibility, it’s an essential tool for type-safe application development.

← Back to list