zod-validation

This skill covers Zod v3+ patterns for building type-safe validation schemas for forms, APIs, and data parsing.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "zod-validation" with this command: npx skills add tejovanthn/rasikalife/tejovanthn-rasikalife-zod-validation

Zod Validation

This skill covers Zod v3+ patterns for building type-safe validation schemas for forms, APIs, and data parsing.

Core Concepts

Zod Benefits:

  • TypeScript-first schema validation

  • Runtime type checking

  • Type inference from schemas

  • Composable and reusable schemas

  • Rich error messages

  • Zero dependencies

Key Terms:

  • Schema: Validation definition (z.object, z.string, etc.)

  • Parse: Validate and return typed data

  • SafeParse: Validate without throwing errors

  • Transform: Convert data after validation

  • Refine: Custom validation logic

Installation

npm install zod

Basic Types

Primitives

import { z } from "zod";

// String const nameSchema = z.string(); const emailSchema = z.string().email(); const urlSchema = z.string().url(); const uuidSchema = z.string().uuid();

// Number const ageSchema = z.number(); const priceSchema = z.number().positive(); const scoreSchema = z.number().min(0).max(100); const integerSchema = z.number().int();

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

// Date const birthDateSchema = z.date(); const futureSchema = z.date().min(new Date());

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

// Symbol const symSchema = z.symbol();

// Undefined, Null, Void const undefinedSchema = z.undefined(); const nullSchema = z.null(); const voidSchema = z.void();

// Any, Unknown, Never const anySchema = z.any(); // ⚠️ Avoid if possible const unknownSchema = z.unknown(); const neverSchema = z.never();

String Validations

const schema = z.string() .min(3, "Must be at least 3 characters") .max(50, "Must be at most 50 characters") .email("Invalid email address") .url("Invalid URL") .uuid("Invalid UUID") .regex(/^[a-z0-9_]+$/, "Only lowercase letters, numbers, and underscores") .startsWith("https://", "Must start with https://") .endsWith(".com", "Must end with .com") .includes("@", "Must include @") .trim() // Remove whitespace .toLowerCase() // Convert to lowercase .toUpperCase(); // Convert to uppercase

// Custom error messages const emailSchema = z.string().email({ message: "Please enter a valid email address", });

// Length const exactLength = z.string().length(5, "Must be exactly 5 characters");

// Date strings const dateString = z.string().datetime(); // ISO 8601 const dateOnly = z.string().date(); // YYYY-MM-DD const timeOnly = z.string().time(); // HH:mm:ss

// IP addresses const ipv4 = z.string().ip({ version: "v4" }); const ipv6 = z.string().ip({ version: "v6" });

Number Validations

const schema = z.number() .min(0, "Must be at least 0") .max(100, "Must be at most 100") .positive("Must be positive") .negative("Must be negative") .nonnegative("Must be 0 or greater") .nonpositive("Must be 0 or less") .int("Must be an integer") .multipleOf(5, "Must be a multiple of 5") .finite("Must be finite") .safe("Must be a safe integer");

// Coerce string to number const coercedNumber = z.coerce.number(); // "123" → 123

Object Schemas

Basic Objects

const userSchema = z.object({ id: z.string().uuid(), email: z.string().email(), name: z.string().min(1), age: z.number().int().min(0).max(120), verified: z.boolean(), role: z.enum(["ADMIN", "USER", "GUEST"]), createdAt: z.date(), });

// Type inference type User = z.infer<typeof userSchema>; // { // id: string; // email: string; // name: string; // age: number; // verified: boolean; // role: "ADMIN" | "USER" | "GUEST"; // createdAt: Date; // }

Optional and Nullable

const schema = z.object({ // Optional (can be undefined) bio: z.string().optional(),

// Nullable (can be null) avatar: z.string().url().nullable(),

// Nullable and optional (can be null or undefined) middleName: z.string().nullable().optional(),

// With default value role: z.string().default("USER"),

// Default from function createdAt: z.date().default(() => new Date()), });

Nested Objects

const addressSchema = z.object({ street: z.string(), city: z.string(), state: z.string().length(2), zipCode: z.string().regex(/^\d{5}$/), });

const userSchema = z.object({ name: z.string(), email: z.string().email(), address: addressSchema, // Or inline settings: z.object({ notifications: z.boolean(), theme: z.enum(["light", "dark"]), }), });

Partial, Required, Pick, Omit

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

// Make all fields optional const partialUser = userSchema.partial(); type PartialUser = z.infer<typeof partialUser>; // { id?: string; email?: string; name?: string; bio?: string }

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

// Pick specific fields const userCredentials = userSchema.pick({ email: true, password: true });

// Omit fields const publicUser = userSchema.omit({ password: true });

// Make specific fields optional const updateSchema = userSchema.partial({ bio: true });

Extend and Merge

const baseSchema = z.object({ id: z.string(), createdAt: z.date(), });

// Extend (adds fields) const userSchema = baseSchema.extend({ email: z.string().email(), name: z.string(), });

// Merge (combines schemas) const timestampSchema = z.object({ createdAt: z.date(), updatedAt: z.date(), });

const fullSchema = userSchema.merge(timestampSchema);

Arrays and Tuples

Arrays

// Array of strings const tagsSchema = z.array(z.string());

// Array with constraints const schema = z.array(z.string()) .min(1, "At least one tag required") .max(5, "Maximum 5 tags allowed") .nonempty("Array cannot be empty");

// Array of objects const usersSchema = z.array( z.object({ id: z.string(), name: z.string(), }) );

// Type inference type Users = z.infer<typeof usersSchema>; // Array<{ id: string; name: string }>

Tuples

// Fixed-length array with specific types const coordinatesSchema = z.tuple([ z.number(), // latitude z.number(), // longitude ]);

type Coordinates = z.infer<typeof coordinatesSchema>; // [number, number]

// With rest parameters const mixedSchema = z.tuple([ z.string(), // first element is string z.number(), // second element is number ]).rest(z.boolean()); // rest are booleans

// ["hello", 42, true, false, true]

Enums and Literals

Enums

// Native enum const schema = z.enum(["ADMIN", "USER", "GUEST"]);

type Role = z.infer<typeof schema>; // "ADMIN" | "USER" | "GUEST"

// Access enum values schema.enum.ADMIN; // "ADMIN" schema.options; // ["ADMIN", "USER", "GUEST"]

// TypeScript enum enum UserRole { ADMIN = "ADMIN", USER = "USER", GUEST = "GUEST", }

const roleSchema = z.nativeEnum(UserRole);

Literals

// Single literal value const trueSchema = z.literal(true); const adminSchema = z.literal("ADMIN"); const numberSchema = z.literal(42);

// Use with union for multiple values const statusSchema = z.union([ z.literal("pending"), z.literal("approved"), z.literal("rejected"), ]);

// Or use enum const statusEnum = z.enum(["pending", "approved", "rejected"]);

Unions and Discriminated Unions

Basic Unions

// String or number const idSchema = z.union([z.string(), z.number()]);

// Null or string const nullableString = z.union([z.string(), z.null()]); // Or use .nullable() const nullableString2 = z.string().nullable();

Discriminated Unions

// Better type inference for unions const eventSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("click"), x: z.number(), y: z.number(), }), z.object({ type: z.literal("keypress"), key: z.string(), }), z.object({ type: z.literal("focus"), element: z.string(), }), ]);

type Event = z.infer<typeof eventSchema>; // { type: "click"; x: number; y: number } // | { type: "keypress"; key: string } // | { type: "focus"; element: string }

// TypeScript knows which fields are available based on type function handleEvent(event: Event) { if (event.type === "click") { console.log(event.x, event.y); // TypeScript knows x and y exist } else if (event.type === "keypress") { console.log(event.key); // TypeScript knows key exists } }

Custom Validation

Refine

// Single refinement const passwordSchema = z .string() .min(8) .refine( (password) => /[A-Z]/.test(password), { message: "Password must contain an uppercase letter" } ) .refine( (password) => /[a-z]/.test(password), { message: "Password must contain a lowercase letter" } ) .refine( (password) => /[0-9]/.test(password), { message: "Password must contain a number" } );

// Multi-field refinement 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 attached to this field } );

// Async refinement const emailSchema = z.string().email().refine( async (email) => { const user = await db.user.findUnique({ where: { email } }); return !user; // true if email is available }, { message: "Email already registered" } );

Transform

// Transform after validation const trimmedString = z.string().transform((str) => str.trim());

const numberFromString = z.string().transform((str) => parseInt(str, 10));

// Or use coerce const coercedNumber = z.coerce.number(); // "123" → 123

// Complex transformation const userSchema = z .object({ email: z.string().email(), name: z.string(), }) .transform((data) => ({ ...data, email: data.email.toLowerCase(), displayName: data.name.toUpperCase(), }));

// Async transform const uploadSchema = z .instanceof(File) .transform(async (file) => { const url = await uploadToS3(file); return { url, size: file.size }; });

Superrefine (Advanced)

const schema = z.object({ age: z.number(), hasGuardian: z.boolean(), guardianName: z.string().optional(), }).superRefine((data, ctx) => { if (data.age < 18 && !data.hasGuardian) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Guardian required for users under 18", path: ["hasGuardian"], }); }

if (data.hasGuardian && !data.guardianName) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Guardian name is required", path: ["guardianName"], }); } });

Parsing and Validation

Parse (Throws on Error)

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

try { const user = userSchema.parse({ email: "user@example.com", age: 25, }); // user is typed as { email: string; age: number } } catch (error) { if (error instanceof z.ZodError) { console.error(error.errors); } }

SafeParse (Returns Result Object)

const result = userSchema.safeParse({ email: "invalid", age: "not a number", });

if (result.success) { const user = result.data; // user is typed correctly } else { const errors = result.error.errors; // [ // { // code: "invalid_string", // message: "Invalid email", // path: ["email"], // }, // { // code: "invalid_type", // message: "Expected number, received string", // path: ["age"], // } // ] }

Async Parsing

// For async transforms or refinements const result = await schema.parseAsync(data); const result = await schema.safeParseAsync(data);

Error Handling

Error Structure

try { userSchema.parse(invalidData); } catch (error) { if (error instanceof z.ZodError) { // error.errors is an array of issues error.errors.forEach((issue) => { console.log(issue.path); // ["email"] console.log(issue.message); // "Invalid email" console.log(issue.code); // "invalid_string" });

// Formatted errors
const formatted = error.format();
// {
//   email: { _errors: ["Invalid email"] },
//   age: { _errors: ["Expected number, received string"] }
// }

// Flattened errors
const flattened = error.flatten();
// {
//   formErrors: [],
//   fieldErrors: {
//     email: ["Invalid email"],
//     age: ["Expected number, received string"]
//   }
// }

} }

Custom Error Messages

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

age: z.number({ required_error: "Age is required", invalid_type_error: "Age must be a number", }).min(18, "Must be at least 18 years old"), });

// Global error map z.setErrorMap((issue, ctx) => { if (issue.code === z.ZodIssueCode.invalid_type) { if (issue.expected === "string") { return { message: "This field must be text" }; } } return { message: ctx.defaultError }; });

Reusable Schemas

Schema Composition

// Base schemas const emailSchema = z.string().email(); const passwordSchema = z.string().min(8).regex(/[A-Z]/).regex(/[0-9]/); const timestampSchema = z.object({ createdAt: z.date().default(() => new Date()), updatedAt: z.date().default(() => new Date()), });

// Compose into larger schemas const loginSchema = z.object({ email: emailSchema, password: passwordSchema, });

const userSchema = z.object({ id: z.string().uuid(), email: emailSchema, name: z.string(), }).merge(timestampSchema);

Schema Factory

// Generic pagination schema function paginatedSchema<T extends z.ZodTypeAny>(itemSchema: T) { return z.object({ items: z.array(itemSchema), total: z.number(), page: z.number(), pageSize: z.number(), }); }

// Usage const userSchema = z.object({ id: z.string(), name: z.string() }); const paginatedUsers = paginatedSchema(userSchema);

type PaginatedUsers = z.infer<typeof paginatedUsers>; // { // items: Array<{ id: string; name: string }>; // total: number; // page: number; // pageSize: number; // }

Shared Validation Schemas

// packages/core/src/schemas.ts export const emailSchema = z.string().email(); export const phoneSchema = z.string().regex(/^+?[1-9]\d{1,14}$/); export const uuidSchema = z.string().uuid();

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

export const userCreateSchema = z.object({ email: emailSchema, name: z.string().min(1).max(100), phone: phoneSchema.optional(), });

export const userUpdateSchema = userCreateSchema.partial();

// Use in multiple packages // packages/functions/src/user/router.ts import { userCreateSchema } from "@myapp/core";

export const userRouter = router({ create: protectedProcedure .input(userCreateSchema) .mutation(async ({ input }) => { // input is typed from schema }), });

Advanced Patterns

Branded Types

// Create nominal types const userId = z.string().uuid().brand("UserId"); const email = z.string().email().brand("Email");

type UserId = z.infer<typeof userId>; // string & { __brand: "UserId" } type Email = z.infer<typeof email>; // string & { __brand: "Email" }

// Prevents mixing different string types function getUser(id: UserId) { /* ... */ }

const validId = userId.parse("550e8400-e29b-41d4-a716-446655440000"); getUser(validId); // ✓ OK

const regularString = "550e8400-e29b-41d4-a716-446655440000"; getUser(regularString); // ✗ Type error

Lazy Schemas (Recursive)

// For recursive types type Category = { name: string; subcategories: Category[]; };

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

Catch (Fallback Values)

// Provide fallback on parse failure const schema = z.string().catch("default value");

const result = schema.parse(123); // "default value"

// With function const dateSchema = z.date().catch(() => new Date());

Pipe (Chain Schemas)

// Parse then transform const schema = z.string().pipe(z.coerce.number());

const result = schema.parse("123"); // 123 (number)

// Multi-step validation const trimmedEmail = z .string() .transform((s) => s.trim()) .pipe(z.string().email());

Integration Patterns

With Conform (Forms)

import { parseWithZod } from "@conform-to/zod";

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

export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const submission = parseWithZod(formData, { schema });

if (submission.status !== "success") { return json({ submission: submission.reply() }); }

// submission.value is typed from schema await login(submission.value); return redirect("/dashboard"); }

With tRPC

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

const userRouter = router({ create: publicProcedure .input( z.object({ email: z.string().email(), name: z.string().min(1), }) ) .output( z.object({ id: z.string(), email: z.string(), name: z.string(), }) ) .mutation(async ({ input }) => { // input is typed return await createUser(input); }), });

With Prisma

import { Prisma } from "@prisma/client";

// Validate before database operation const userCreateSchema = z.object({ email: z.string().email(), name: z.string(), }) satisfies z.ZodType<Prisma.UserCreateInput>;

Best Practices

  • Reuse schemas: Create shared schemas in core package

  • Type from schemas: Use z.infer instead of duplicating types

  • Composable schemas: Build complex schemas from simple ones

  • Async sparingly: Use async refinements only on server

  • Custom errors: Provide helpful, user-friendly error messages

  • Transform carefully: Keep transformations simple and predictable

  • Test schemas: Unit test complex validation logic

  • Use safeParse: Prefer safeParse over parse to avoid exceptions

  • Branded types: Use for nominal typing when needed

  • Document schemas: Add JSDoc comments to complex schemas

Common Gotcas

  • Parse vs safeParse: parse throws, safeParse returns result

  • Optional vs nullable: optional = undefined, nullable = null

  • Async transforms: Must use parseAsync/safeParseAsync

  • Transform order: Transforms run after validation

  • Error paths: Use path in refine for specific field errors

  • Coerce vs transform: coerce is simpler for type conversion

  • Default values: Apply before validation

  • Array validation: Min/max checks array length, not items

  • Union types: Discriminated unions have better inference

  • File validation: Use z.instanceof(File), not z.object()

Resources

  • Zod Documentation

  • Zod GitHub

  • TypeScript Handbook

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

email-templates

No summary provided by upstream source.

Repository SourceNeeds Review
General

marketing-copy

No summary provided by upstream source.

Repository SourceNeeds Review
General

conform

No summary provided by upstream source.

Repository SourceNeeds Review