Zod

serializationTypeScriptvalidationschematype-saferuntime-verification

Library

Zod

Overview

Zod is a TypeScript-first schema declaration and validation library. Through static type inference, it automatically generates TypeScript types from schema definitions, achieving both runtime type validation and compile-time type safety. It's ideal for safely ingesting external data such as API response validation, form validation, and environment variable type checking. With its intuitive API and excellent error messages, it significantly enhances the developer experience.

Details

Zod is a schema validation library that maximizes the use of TypeScript's type system. By generating both TypeScript types and runtime validators from a single schema definition, it eliminates type definition duplication. Its design based on functional programming principles makes schema composition and transformation easy, enabling support for complex data structures. With two APIs - parse (throws exceptions) and safeParse (returns result objects) - error handling is also flexible.

Key Features

  • TypeScript Type Inference: Automatically generates types from schemas
  • Comprehensive Type Support: Primitives, objects, arrays, unions, intersections, etc.
  • Custom Validation: Flexible validation logic through refine method
  • Error Handling: Detailed and clear error messages
  • Transformation: Data transformation via transform and preprocess
  • Async Validation: Validation using external APIs is possible

Pros and Cons

Pros

  • Perfect integration with TypeScript (powerful type inference)
  • Zero dependencies and lightweight (about 8KB gzipped)
  • Intuitive and readable API design
  • Detailed error information with customizable messages
  • Easy schema composition and reuse
  • Active development and community support

Cons

  • Runtime overhead exists
  • High initialization cost for large schemas
  • No direct compatibility with JSON Schema
  • Some advanced types (conditional types, etc.) are difficult to express
  • Bundle size may be a concern
  • Somewhat steep learning curve (for advanced features)

References

Code Examples

Basic Schema Definition and Validation

import { z } from 'zod';

// Schema definition
const UserSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(1).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  isActive: z.boolean().default(true),
  roles: z.array(z.string()).optional()
});

// Automatic TypeScript type generation
type User = z.infer<typeof UserSchema>;

// Execute validation
try {
  const user = UserSchema.parse({
    id: 1,
    name: "John Doe",
    email: "[email protected]",
    age: 30
  });
  console.log("Valid user:", user);
} catch (error) {
  if (error instanceof z.ZodError) {
    console.error("Validation errors:", error.errors);
  }
}

// Safe parsing
const result = UserSchema.safeParse(userData);
if (result.success) {
  console.log("Valid data:", result.data);
} else {
  console.error("Validation failed:", result.error.format());
}

API Response Validation

import { z } from 'zod';

// API response schema
const ApiResponseSchema = z.object({
  status: z.literal('success').or(z.literal('error')),
  data: z.object({
    users: z.array(z.object({
      id: z.string().uuid(),
      name: z.string(),
      createdAt: z.string().datetime()
    })),
    total: z.number(),
    page: z.number(),
    pageSize: z.number()
  }).optional(),
  error: z.object({
    code: z.string(),
    message: z.string()
  }).optional()
});

// API call and validation
async function fetchUsers(page: number) {
  const response = await fetch(`/api/users?page=${page}`);
  const json = await response.json();
  
  // Validate response
  const validatedData = ApiResponseSchema.parse(json);
  
  if (validatedData.status === 'success' && validatedData.data) {
    return validatedData.data.users;
  } else if (validatedData.error) {
    throw new Error(validatedData.error.message);
  }
}

Custom Validation and Transformation

import { z } from 'zod';

// Custom validation
const PasswordSchema = z
  .string()
  .min(8, "Password must be at least 8 characters")
  .refine(
    (password) => /[A-Z]/.test(password),
    "Must contain at least one uppercase letter"
  )
  .refine(
    (password) => /[0-9]/.test(password),
    "Must contain at least one number"
  )
  .refine(
    (password) => /[!@#$%^&*]/.test(password),
    "Must contain at least one special character"
  );

// Data transformation
const DateSchema = z
  .string()
  .transform((str) => new Date(str))
  .refine((date) => !isNaN(date.getTime()), {
    message: "Invalid date format"
  });

// Schema with preprocessing
const TrimmedString = z.preprocess(
  (val) => typeof val === 'string' ? val.trim() : val,
  z.string()
);

// Usage examples
const password = PasswordSchema.parse("MyP@ssw0rd");
const date = DateSchema.parse("2024-01-15");
const trimmed = TrimmedString.parse("  hello world  "); // "hello world"

Form Validation

import { z } from 'zod';

// Form schema
const RegistrationFormSchema = z.object({
  username: z
    .string()
    .min(3, "Username must be at least 3 characters")
    .max(20, "Username must be 20 characters or less")
    .regex(/^[a-zA-Z0-9_]+$/, "Only alphanumeric and underscore allowed"),
  
  email: z
    .string()
    .email("Please enter a valid email address"),
  
  password: z
    .string()
    .min(8, "Password must be at least 8 characters"),
  
  confirmPassword: z.string(),
  
  age: z
    .number()
    .int("Please enter an integer")
    .min(18, "Must be at least 18 years old"),
  
  terms: z
    .boolean()
    .refine((val) => val === true, "You must agree to the terms")
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ["confirmPassword"]
});

// Validation function
function validateForm(formData: unknown) {
  const result = RegistrationFormSchema.safeParse(formData);
  
  if (!result.success) {
    // Get per-field errors
    const fieldErrors = result.error.flatten().fieldErrors;
    return { success: false, errors: fieldErrors };
  }
  
  return { success: true, data: result.data };
}

Type-Safe Environment Variable Management

import { z } from 'zod';

// Environment variable schema
const EnvSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.string().transform(Number).pipe(z.number().positive()),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  ENABLE_LOGGING: z
    .string()
    .transform((val) => val === 'true')
    .pipe(z.boolean())
    .default('false'),
  MAX_CONNECTIONS: z
    .string()
    .optional()
    .transform((val) => val ? Number(val) : 10)
    .pipe(z.number().positive())
});

// Validate environment variables
const env = EnvSchema.parse(process.env);

// Type-safe access
export const config = {
  nodeEnv: env.NODE_ENV,
  port: env.PORT,
  databaseUrl: env.DATABASE_URL,
  apiKey: env.API_KEY,
  enableLogging: env.ENABLE_LOGGING,
  maxConnections: env.MAX_CONNECTIONS
} as const;

// Usage example
if (config.nodeEnv === 'production') {
  // Production environment settings
}

Complex Type Composition and Extension

import { z } from 'zod';

// Base schema
const BaseProductSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  price: z.number().positive()
});

// Extended schema
const DetailedProductSchema = BaseProductSchema.extend({
  description: z.string(),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()),
  stock: z.number().int().nonnegative()
});

// Union types
const NotificationSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('email'),
    to: z.string().email(),
    subject: z.string(),
    body: z.string()
  }),
  z.object({
    type: z.literal('sms'),
    phoneNumber: z.string().regex(/^\+[1-9]\d{1,14}$/),
    message: z.string().max(160)
  }),
  z.object({
    type: z.literal('push'),
    deviceToken: z.string(),
    title: z.string(),
    body: z.string(),
    data: z.record(z.string()).optional()
  })
]);

// Conditional schema
const PaymentSchema = z
  .object({
    method: z.enum(['credit_card', 'bank_transfer', 'paypal']),
    amount: z.number().positive()
  })
  .and(
    z.union([
      z.object({
        method: z.literal('credit_card'),
        cardNumber: z.string().length(16),
        cvv: z.string().length(3)
      }),
      z.object({
        method: z.literal('bank_transfer'),
        accountNumber: z.string(),
        routingNumber: z.string()
      }),
      z.object({
        method: z.literal('paypal'),
        email: z.string().email()
      })
    ])
  );

// Usage example
const notification = NotificationSchema.parse({
  type: 'email',
  to: '[email protected]',
  subject: 'Welcome!',
  body: 'Thank you for signing up.'
});