validation

SKILL.md

Zod Validation Patterns

Type-safe schema validation with automatic TypeScript inference.

Instructions

1. Basic Schemas

import { z } from 'zod';

// Primitives
const nameSchema = z.string().min(2).max(50);
const ageSchema = z.number().int().positive();
const emailSchema = z.string().email();
const urlSchema = z.string().url();

// Objects
const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2),
  email: z.string().email(),
  age: z.number().int().min(18).optional(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.coerce.date(),
});

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

// Usage
const result = userSchema.safeParse(data);
if (result.success) {
  const user: User = result.data;
} else {
  console.error(result.error.format());
}

2. Complex Schemas

// Arrays
const tagsSchema = z.array(z.string()).min(1).max(10);

// Unions
const statusSchema = z.union([
  z.literal('pending'),
  z.literal('active'),
  z.literal('completed'),
]);

// Discriminated unions
const notificationSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('email'), email: z.string().email() }),
  z.object({ type: z.literal('sms'), phone: z.string() }),
  z.object({ type: z.literal('push'), deviceId: z.string() }),
]);

// Recursive (e.g., comments with replies)
const commentSchema: z.ZodType<Comment> = z.lazy(() =>
  z.object({
    id: z.string(),
    text: z.string(),
    replies: z.array(commentSchema),
  })
);

3. Transformations

// Transform to different type
const dateStringSchema = z.string().transform((str) => new Date(str));

// Coerce types
const numberFromString = z.coerce.number(); // "123" → 123
const dateFromString = z.coerce.date();     // "2024-01-01" → Date

// Preprocess
const trimmedString = z.preprocess(
  (val) => (typeof val === 'string' ? val.trim() : val),
  z.string()
);

// Default values
const configSchema = z.object({
  port: z.number().default(3000),
  host: z.string().default('localhost'),
  debug: z.boolean().default(false),
});

4. API Response Validation

// Define response schema
const apiResponseSchema = z.object({
  success: z.boolean(),
  data: z.object({
    users: z.array(userSchema),
    total: z.number(),
    page: z.number(),
  }),
});

// Validate API response
async function fetchUsers(): Promise<User[]> {
  const response = await fetch('/api/users');
  const json = await response.json();
  
  const result = apiResponseSchema.safeParse(json);
  if (!result.success) {
    throw new Error(`Invalid API response: ${result.error.message}`);
  }
  
  return result.data.data.users;
}

5. Form Validation (React Hook Form)

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

const loginSchema = z.object({
  email: z.string()
    .email('Invalid email')
    .min(1, 'Email is required'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/[A-Z]/, 'Must contain uppercase')
    .regex(/[0-9]/, 'Must contain number'),
  rememberMe: z.boolean().optional(),
});

type LoginForm = z.infer<typeof loginSchema>;

function LoginForm() {
  const form = useForm<LoginForm>({
    resolver: zodResolver(loginSchema),
    defaultValues: { email: '', password: '', rememberMe: false },
  });
  
  const onSubmit = (data: LoginForm) => {
    // data is fully typed and validated
  };
}

6. Environment Variables

// env.ts
const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  PORT: z.coerce.number().default(3000),
  DEBUG: z.coerce.boolean().default(false),
});

// Parse with error handling
function getEnv() {
  const result = envSchema.safeParse(process.env);
  if (!result.success) {
    console.error('❌ Invalid environment variables:');
    console.error(result.error.format());
    process.exit(1);
  }
  return result.data;
}

export const env = getEnv();

// Usage
console.log(env.DATABASE_URL); // Fully typed!

7. Custom Error Messages

const userSchema = z.object({
  username: z.string({
    required_error: 'Username is required',
    invalid_type_error: 'Username must be a string',
  })
    .min(3, { message: 'Username must be at least 3 characters' })
    .max(20, { message: 'Username must be at most 20 characters' })
    .regex(/^[a-z0-9_]+$/, { message: 'Only lowercase letters, numbers, and underscores' }),
    
  email: z.string()
    .email({ message: 'Please enter a valid email address' }),
    
  age: z.number()
    .min(18, { message: 'You must be at least 18 years old' }),
});

// Get formatted errors
const result = userSchema.safeParse(data);
if (!result.success) {
  const formatted = result.error.format();
  // formatted.username?._errors
  // formatted.email?._errors
}

8. Reusable Schemas

// lib/schemas/common.ts
export const idSchema = z.string().uuid();
export const emailSchema = z.string().email().toLowerCase();
export const phoneSchema = z.string().regex(/^\+?[1-9]\d{9,14}$/);
export const urlSchema = z.string().url();
export const slugSchema = z.string().regex(/^[a-z0-9-]+$/);

// Pagination
export const paginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(20),
});

// lib/schemas/user.ts
export const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: emailSchema,
  password: z.string().min(8),
});

export const updateUserSchema = createUserSchema.partial();

9. Extend and Merge

// Base schema
const baseUserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
});

// Extend
const fullUserSchema = baseUserSchema.extend({
  name: z.string(),
  role: z.enum(['admin', 'user']),
});

// Merge
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
});

const userWithAddressSchema = fullUserSchema.merge(addressSchema);

// Pick / Omit
const publicUserSchema = fullUserSchema.pick({ id: true, name: true });
const createUserSchema = fullUserSchema.omit({ id: true });

Best Practices

Do Don't
✅ Use safeParse() ❌ Use parse() without try/catch
✅ Define schemas in separate files ❌ Inline schemas everywhere
✅ Use z.infer<> for types ❌ Manually define duplicate types
✅ Add custom error messages ❌ Use default cryptic errors

References

Weekly Installs
9
First Seen
Feb 3, 2026
Installed on
opencode9
gemini-cli9
antigravity9
claude-code9
codex9
cursor9