Search

PythonReactSEOPDF
GitHub Skill

zod

Validates data with Zod schemas including type inference, transformations, error handling, and form integration. Use when validating API inputs, form data, environment variables, or any runtime data validation.

mgd34msu☆ 3 Repository stars0 Repository forks

Installation

Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.

View source ↗Guides

Skill instructions

Zod

TypeScript-first schema validation with static type inference.

Quick Start

Install:

npm install zod

Basic usage:

import { z } from 'zod';

// Define schema
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(18),
});

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

// Validate data
const result = UserSchema.safeParse(data);
if (result.success) {
  console.log(result.data); // Typed as User
} else {
  console.log(result.error.issues);
}

Primitive Types

// Strings
const name = z.string();
const email = z.string().email();
const url = z.string().url();
const uuid = z.string().uuid();
const cuid = z.string().cuid();
const regex = z.string().regex(/^[a-z]+$/);
const minMax = z.string().min(1).max(100);
const length = z.string().length(10);
const nonempty = z.string().min(1, 'Required');

// Numbers
const age = z.number();
const positive = z.number().positive();
const negative = z.number().negative();
const integer = z.number().int();
const range = z.number().min(0).max(100);
const finite = z.number().finite();

// BigInt
const bigint = z.bigint();

// Boolean
const active = z.boolean();

// Date
const date = z.date();
const minDate = z.date().min(new Date('2024-01-01'));
const maxDate = z.date().max(new Date());

// Undefined/Null
const undef = z.undefined();
const nul = z.null();
const nullable = z.string().nullable(); // string | null
const optional = z.string().optional(); // string | undefined
const nullish = z.string().nullish();   // string | null | undefined

Objects

Basic Objects
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().optional(),
});

type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; age?: number }
Modifiers
const schema = z.object({
  name: z.string(),
  email: z.string(),
  age: z.number(),
});

// All properties optional
const PartialSchema = schema.partial();
// { name?: string; email?: string; age?: number }

// All properties required
const RequiredSchema = schema.required();

// Pick specific properties
const NameEmail = schema.pick({ name: true, email: true });
// { name: string; email: string }

// Omit specific properties
const NoAge = schema.omit({ age: true });
// { name: string; email: string }

// Extend with new properties
const ExtendedSchema = schema.extend({
  role: z.enum(['admin', 'user']),
});

// Merge schemas
const merged = schema.merge(z.object({ role: z.string() }));

// Make specific properties optional
const PartialAge = schema.partial({ age: true });
// { name: string; email: string; age?: number }
Strict and Passthrough
// Strict: fail if unknown keys present
const StrictSchema = z.object({ name: z.string() }).strict();

// Passthrough: allow and preserve unknown keys
const PassthroughSchema = z.object({ name: z.string() }).passthrough();

// Strip: remove unknown keys (default)
const StripSchema = z.object({ name: z.string() }).strip();

Arrays and Tuples

// Arrays
const strings = z.array(z.string());
const numbers = z.number().array(); // Alternative syntax
const nonEmpty = z.array(z.string()).nonempty();
const lengthRange = z.array(z.string()).min(1).max(10);
const exactLength = z.array(z.string()).length(5);

// Tuples (fixed length, specific types)
const tuple = z.tuple([z.string(), z.number(), z.boolean()]);
// [string, number, boolean]

// Tuple with rest
const tupleWithRest = z.tuple([z.string()]).rest(z.number());
// [string, ...number[]]

Unions and Enums

Unions
// Union types
const StringOrNumber = z.union([z.string(), z.number()]);
// string | number

// Shorthand
const StringOrNumber2 = z.string().or(z.number());

// Discriminated unions (better performance, error messages)
const Result = z.discriminatedUnion('status', [
  z.object({ status: z.literal('success'), data: z.string() }),
  z.object({ status: z.literal('error'), error: z.string() }),
]);
Enums
// Zod enum
const RoleSchema = z.enum(['admin', 'user', 'guest']);
type Role = z.infer<typeof RoleSchema>; // 'admin' | 'user' | 'guest'

// Get enum values
RoleSchema.options; // ['admin', 'user', 'guest']

// Native enum
enum Status {
  Active = 'active',
  Inactive = 'inactive',
}
const StatusSchema = z.nativeEnum(Status);
Literals
const admin = z.literal('admin');
const fortyTwo = z.literal(42);
const trueLiteral = z.literal(true);

Transformations

Transform
// Transform string to number
const StringToNumber = z.string().transform((val) => parseInt(val, 10));

// Parse and transform
const DateString = z.string().transform((val) => new Date(val));

// With validation after transform
const PositiveNumber = z.string()
  .transform((val) => parseInt(val, 10))
  .pipe(z.number().positive());
Coercion
// Automatic coercion
const coercedString = z.coerce.string();   // Any -> string
const coercedNumber = z.coerce.number();   // Any -> number
const coercedBoolean = z.coerce.boolean(); // Any -> boolean
const coercedDate = z.coerce.date();       // Any -> Date

// Common use: form data
const FormSchema = z.object({
  age: z.coerce.number().min(0).max(120),
  active: z.coerce.boolean(),
  date: z.coerce.date(),
});
Preprocess
// Run function before validation
const TrimmedString = z.preprocess(
  (val) => (typeof val === 'string' ? val.trim() : val),
  z.string()
);

// Handle null/undefined
const NullableToDefault = z.preprocess(
  (val) => val ?? 'default',
  z.string()
);
Default Values
const WithDefault = z.string().default('default value');
const WithDefaultFn = z.string().default(() => generateId());

// Catch: use default on parse failure
const SafeNumber = z.number().catch(0);
const SafeString = z.string().catch('');

Refinements

Custom Validation
// Simple refinement
const PasswordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .refine((val) => /[A-Z]/.test(val), {
    message: 'Password must contain uppercase letter',
  })
  .refine((val) => /[0-9]/.test(val), {
    message: 'Password must contain number',
  });

// Superrefine (multiple issues)
const PasswordSchema2 = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: z.ZodIssueCode.too_small,
      minimum: 8,
      type: 'string',
      inclusive: true,
      message: 'Password must be at least 8 characters',
    });
  }

  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'Password must contain uppercase letter',
    });
  }
});
Cross-Field Validation
const SignupSchema = z.object({
  password: z.string().min(8),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: "Passwords don't match",
  path: ['confirmPassword'], // Error on specific field
});

Error Handling

Safe Parse
const result = UserSchema.safeParse(data);

if (result.success) {
  // result.data is typed
  console.log(result.data);
} else {
  // result.error is ZodError
  console.log(result.error.issues);
}
Parse (throws)
try {
  const user = UserSchema.parse(data);
  // user is typed
} catch (error) {
  if (error instanceof z.ZodError) {
    console.log(error.issues);
  }
}
Format Errors
const result = UserSchema.safeParse(data);

if (!result.success) {
  // Flat format
  const flat = result.error.flatten();
  // { formErrors: string[], fieldErrors: { [key]: string[] } }

  // Formatted
  const formatted = result.error.format();
  // { _errors: string[], field: { _errors: string[] } }

  // Custom format
  const issues = result.error.issues.map((issue) => ({
    path: issue.path.join('.'),
    message: issue.message,
  }));
}
Custom Error Messages
const UserSchema = z.object({
  name: z.string({
    required_error: 'Name is required',
    invalid_type_error: 'Name must be a string',
  }).min(1, 'Name cannot be empty'),

  email: z.string()
    .email({ message: 'Invalid email format' }),

  age: z.number()
    .min(18, { message: 'Must be 18 or older' })
    .max(120, { message: 'Invalid age' }),
});

Common Patterns

API Request Validation
const CreatePostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
  tags: z.array(z.string()).default([]),
  published: z.boolean().default(false),
});

// In API handler
export async function POST(request: Request) {
  const body = await request.json();
  const result = CreatePostSchema.safeParse(body);

  if (!result.success) {
    return Response.json(
      { error: result.error.flatten().fieldErrors },
      { status: 400 }
    );
  }

  const post = await createPost(result.data);
  return Response.json(post);
}
Environment Variables
const EnvSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
  PORT: z.coerce.number().default(3000),
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});

export const env = EnvSchema.parse(process.env);
Form Data
const ContactFormSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  email: z.string().email('Invalid email'),
  message: z.string().min(10, 'Message too short').max(1000),
  subscribe: z.coerce.boolean().default(false),
});

// Parse FormData
function parseFormData(formData: FormData) {
  return ContactFormSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    message: formData.get('message'),
    subscribe: formData.get('subscribe'),
  });
}
React Hook Form Integration
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';

const FormSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

type FormData = z.infer<typeof FormSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: zodResolver(FormSchema),
  });

  const onSubmit = (data: FormData) => {
    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>}

      <button type="submit">Login</button>
    </form>
  );
}
Server Actions (Next.js)
'use server';

import { z } from 'zod';

const CreatePostSchema = z.object({
  title: z.string().min(1).max(100),
  content: z.string().min(1),
});

export async function createPost(formData: FormData) {
  const result = CreatePostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content'),
  });

  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  const post = await db.posts.create({ data: result.data });
  return { data: post };
}

Type Inference

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

// Infer type from schema
type User = z.infer<typeof UserSchema>;

// Input type (before transforms)
type UserInput = z.input<typeof UserSchema>;

// Output type (after transforms)
type UserOutput = z.output<typeof UserSchema>;

Best Practices

  1. Define schemas once - Reuse across validation points
  2. Use safeParse - Handle errors gracefully
  3. Infer types - Don't duplicate type definitions
  4. Use coercion for forms - Handle string inputs
  5. Custom error messages - User-friendly validation

Common Mistakes

Mistake Fix
Using parse without try/catch Use safeParse instead
Not handling errors Check result.success
Duplicating types Use z.infer<typeof Schema>
Ignoring transforms Remember input vs output types
Over-complex schemas Break into smaller schemas

Reference Files

Examples

Read the usage examples in the source repository.

View source ↗

Source history

Indexed on
2026-01-20
Index updated
2026-01-28
Branch
main

Index and sync dates describe this directory. View the source repository for code history.

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!