Zod v4 Coming Soon - Evolution of TypeScript Type-Safe Validation

2025.12.02

Zod v4 Overview

Zod v4 is the next major version of the TypeScript schema validation library. It includes performance improvements, better error messages, and new utilities.

Performance Improvements

MetricZod v3Zod v4Improvement
Object Parse Speed10.2μs6.8μs33% faster
Array Parse Speed (1000 elements)850μs520μs39% faster
Bundle Size (gzip)14KB10KB29% smaller

New Features

  • Improved error messages
  • Enhanced JSON Schema output
  • Metadata API
  • New primitive types
  • Simplified custom validation

Basic Usage

import { z } from 'zod';

// Schema definition
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(2).max(100),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'guest']),
  preferences: z.object({
    theme: z.enum(['light', 'dark']).default('light'),
    notifications: z.boolean().default(true),
  }).optional(),
  createdAt: z.coerce.date(),
});

// Type inference
type User = z.infer<typeof UserSchema>;
// {
//   id: string;
//   email: string;
//   name: string;
//   age?: number;
//   role: 'admin' | 'user' | 'guest';
//   preferences?: { theme: 'light' | 'dark'; notifications: boolean };
//   createdAt: Date;
// }

// Validation
const result = UserSchema.safeParse(input);
if (result.success) {
  console.log(result.data); // Type-safe
} else {
  console.log(result.error.issues);
}

Improved Error Messages

// Zod v4: More detailed error paths
const OrderSchema = z.object({
  items: z.array(
    z.object({
      productId: z.string().uuid(),
      quantity: z.number().int().positive(),
    })
  ).min(1),
});

const result = OrderSchema.safeParse({
  items: [
    { productId: 'invalid-uuid', quantity: -1 },
  ],
});

// Error output
result.error?.issues.forEach((issue) => {
  console.log({
    path: issue.path.join('.'),  // "items.0.productId"
    message: issue.message,       // "Invalid uuid"
    code: issue.code,             // "invalid_string"
  });
});

Custom Error Messages

const FormSchema = z.object({
  email: z.string({
    required_error: 'Email is required',
    invalid_type_error: 'Email must be a string',
  }).email({
    message: 'Please enter a valid email address',
  }),

  password: z.string()
    .min(8, { message: 'Password must be at least 8 characters' })
    .regex(/[A-Z]/, { message: 'Must include at least one uppercase letter' })
    .regex(/[0-9]/, { message: 'Must include at least one number' }),

  age: z.number({
    required_error: 'Please enter your age',
    invalid_type_error: 'Age must be a number',
  }).int().min(0).max(150),
});

// Internationalized error map
const errorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === 'string') {
      return { message: `Please enter at least ${issue.minimum} characters` };
    }
    if (issue.type === 'number') {
      return { message: `Please enter a value of ${issue.minimum} or more` };
    }
  }
  return { message: ctx.defaultError };
};

z.setErrorMap(errorMap);

New Utility Types

z.coerce (Type Coercion)

// Convert string to number
const NumberSchema = z.coerce.number();
NumberSchema.parse('42');     // 42
NumberSchema.parse('3.14');   // 3.14
NumberSchema.parse(true);     // 1

// Convert string to date
const DateSchema = z.coerce.date();
DateSchema.parse('2024-01-01');           // Date
DateSchema.parse(1704067200000);          // Date
DateSchema.parse(new Date());             // Date

// Convert string to boolean
const BoolSchema = z.coerce.boolean();
BoolSchema.parse('true');     // true
BoolSchema.parse('false');    // false
BoolSchema.parse(1);          // true
BoolSchema.parse(0);          // false

z.pipe (Transform Chain)

// String → Number → Validation
const PriceSchema = z
  .string()
  .pipe(z.coerce.number().positive());

PriceSchema.parse('100');   // 100
PriceSchema.parse('-50');   // Error: Number must be positive

// JSON string parsing
const JsonDataSchema = z
  .string()
  .transform((str) => JSON.parse(str))
  .pipe(z.object({
    name: z.string(),
    value: z.number(),
  }));

JsonDataSchema.parse('{"name": "test", "value": 42}');

z.discriminatedUnion (Discriminated Union)

// Event type discrimination
const EventSchema = z.discriminatedUnion('type', [
  z.object({
    type: z.literal('user_created'),
    userId: z.string(),
    email: z.string().email(),
  }),
  z.object({
    type: z.literal('user_updated'),
    userId: z.string(),
    changes: z.record(z.unknown()),
  }),
  z.object({
    type: z.literal('user_deleted'),
    userId: z.string(),
    deletedAt: z.date(),
  }),
]);

type Event = z.infer<typeof EventSchema>;
// TypeScript discriminatable
function handleEvent(event: Event) {
  switch (event.type) {
    case 'user_created':
      console.log(event.email); // Type-safe
      break;
    case 'user_updated':
      console.log(event.changes);
      break;
    case 'user_deleted':
      console.log(event.deletedAt);
      break;
  }
}

JSON Schema Output

import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

const ProductSchema = z.object({
  id: z.string().uuid().describe('Product unique identifier'),
  name: z.string().min(1).max(255).describe('Product name'),
  price: z.number().positive().describe('Price in cents'),
  category: z.enum(['electronics', 'clothing', 'food']),
  tags: z.array(z.string()).optional(),
  metadata: z.record(z.unknown()).optional(),
});

const jsonSchema = zodToJsonSchema(ProductSchema, {
  name: 'Product',
  target: 'openApi3',
});

// Output:
// {
//   "$schema": "http://json-schema.org/draft-07/schema#",
//   "type": "object",
//   "properties": {
//     "id": {
//       "type": "string",
//       "format": "uuid",
//       "description": "Product unique identifier"
//     },
//     "name": {
//       "type": "string",
//       "minLength": 1,
//       "maxLength": 255,
//       "description": "Product name"
//     },
//     ...
//   },
//   "required": ["id", "name", "price", "category"]
// }

Metadata API

// Attach metadata to schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string(),
}).meta({
  title: 'User',
  description: 'Represents a user in the system',
  examples: [
    { id: '123e4567-e89b-12d3-a456-426614174000', email: 'user@example.com', name: 'John Doe' },
  ],
});

// Get metadata
const metadata = UserSchema._def.meta;
console.log(metadata.title);       // 'User'
console.log(metadata.description); // 'Represents a user in the system'

// Use for OpenAPI generation, etc.
function generateOpenApiSchema(schema: z.ZodType) {
  const meta = schema._def.meta ?? {};
  return {
    title: meta.title,
    description: meta.description,
    examples: meta.examples,
    // ...
  };
}

Branded Types

// Type-level distinction
const UserId = z.string().uuid().brand<'UserId'>();
const PostId = z.string().uuid().brand<'PostId'>();

type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;

// Usage example
function getUser(id: UserId): Promise<User> { ... }
function getPost(id: PostId): Promise<Post> { ... }

const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const postId = PostId.parse('987fcdeb-51a2-34d6-b789-123456789abc');

getUser(userId);  // OK
getUser(postId);  // TypeScript Error!

Form Validation

// Integration with React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const SignUpSchema = z.object({
  email: z.string().email('Please enter a valid email address'),
  password: z.string().min(8, 'Please enter at least 8 characters'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'],
});

type SignUpForm = z.infer<typeof SignUpSchema>;

function SignUpPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<SignUpForm>({
    resolver: zodResolver(SignUpSchema),
  });

  const onSubmit = (data: SignUpForm) => {
    console.log(data);
  };

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

      <input type="password" {...register('password')} />
      {errors.password && <span>{errors.password.message}</span>}

      <input type="password" {...register('confirmPassword')} />
      {errors.confirmPassword && <span>{errors.confirmPassword.message}</span>}

      <button type="submit">Sign Up</button>
    </form>
  );
}

Advanced Patterns

Recursive Schema

// Tree structure
interface Category {
  id: string;
  name: string;
  children: Category[];
}

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

Conditional Validation

const PaymentSchema = z.discriminatedUnion('method', [
  z.object({
    method: z.literal('credit_card'),
    cardNumber: z.string().regex(/^\d{16}$/),
    expiryDate: z.string().regex(/^\d{2}\/\d{2}$/),
    cvv: z.string().regex(/^\d{3,4}$/),
  }),
  z.object({
    method: z.literal('bank_transfer'),
    bankCode: z.string(),
    accountNumber: z.string(),
  }),
  z.object({
    method: z.literal('paypal'),
    email: z.string().email(),
  }),
]);
← Back to list