Zod: TypeScript-First Schema Validation
SkillAI & modelsTypeScript-first schema validation and type inference. Use for validating API requests/responses, form data, env vars, configs, defining type-safe schemas with runtime validation, transforming data, generating JSON Schema for OpenAPI/AI, or encountering missing validation errors, type inference issues, validation error handling problems. Zero dependencies (2kb gzipped).
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Zod: TypeScript-First Schema Validation skill
What this skill tells your AI
The instructions your AI receives, as published by wakita181009/zod-aot in .agents/skills/zod/SKILL.md and read by ahel’s review.
Overview
Zod is a TypeScript-first validation library that enables developers to define schemas for validating data at runtime while automatically inferring static TypeScript types. With zero dependencies and a 2kb core bundle (gzipped), Zod provides immutable, composable validation with comprehensive error handling.
Installation
bun add zod
# or
bun add zod
# or
bun add zod
# or
yarn add zod
Requirements:
- TypeScript v5.5+ with
"strict": trueintsconfig.json - Zod 4.x (4.1.12+)
Important: This skill documents Zod 4.x features. The following APIs require Zod 4 and are NOT available in Zod 3.x:
z.codec()- Bidirectional transformationsz.iso.date(),z.iso.time(),z.iso.datetime(),z.iso.duration()- ISO format validatorsz.toJSONSchema()- JSON Schema generationz.treeifyError(),z.prettifyError(),z.flattenError()- New error formatting helpers.meta()- Enhanced metadata (Zod 3.x only has.describe())- Unified
errorparameter - Replacesmessage,invalid_type_error,required_error,errorMap
For Zod 3.x compatibility or migration guidance, see https://zod.dev
Migrating from Zod v3 to v4
Load references/migration-guide.md for complete v3 to v4 migration documentation.
Quick Summary
Zod v4 introduces breaking changes for better performance:
- Error customization: Use unified
errorparameter (replacesmessage,invalid_type_error,required_error) - Number validation: Stricter - rejects
Infinityand unsafe integers - String formats: Now top-level functions (
z.email()vsz.string().email()) - Object defaults: Applied even in optional fields
- Deprecated APIs: Use
.extend()(not.merge()),z.treeifyError()(noterror.format()) - Function validation: Use
.implement()method - UUID validation: Stricter RFC 9562/4122 compliance
→ Load references/migration-guide.md for: Complete breaking changes, migration checklist, gradual migration strategy, rollback instructions
Core Concepts
Basic Usage Pattern
import { z } from "zod";
// Define schema
const UserSchema = z.object({
username: z.string(),
age: z.number().int().positive(),
email: z.string().email(),
});
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// Validate data (throws on error)
const user = UserSchema.parse(data);
// Validate data (returns result object)
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // Typed!
} else {
console.error(result.error); // ZodError
}
Parsing Methods
Use the appropriate parsing method based on error handling needs:
.parse(data)- ThrowsZodErroron invalid input; returns strongly-typed data on success.safeParse(data)- Returns{ success: true, data }or{ success: false, error }(no exceptions).parseAsync(data)- For schemas with async refinements/transforms.safeParseAsync(data)- Async version that doesn't throw
Best Practice: Use .safeParse() to avoid try-catch blocks and leverage discriminated unions.
Primitive Types
Strings
z.string() // Basic string
z.string().min(5) // Minimum length
z.string().max(100) // Maximum length
z.string().length(10) // Exact length
z.string().email() // Email validation
z.string().url() // URL validation
z.string().uuid() // UUID format
z.string().regex(/^\d+$/) // Custom pattern
z.string().startsWith("pre") // Prefix check
z.string().endsWith("suf") // Suffix check
z.string().trim() // Auto-trim whitespace
z.string().toLowerCase() // Auto-lowercase
z.string().toUpperCase() // Auto-uppercase
// ISO formats (Zod 4+)
z.iso.date() // YYYY-MM-DD
z.iso.time() // HH:MM:SS
z.iso.datetime() // ISO 8601 datetime
z.iso.duration() // ISO 8601 duration
// Network formats
z.ipv4() // IPv4 address
z.ipv6() // IPv6 address
z.cidrv4() // IPv4 CIDR notation
z.cidrv6() // IPv6 CIDR notation
// Other formats
z.jwt() // JWT token
z.nanoid() // Nanoid
z.cuid() // CUID
z.cuid2() // CUID2
z.ulid() // ULID
z.base64() // Base64 encoded
z.hex() // Hexadecimal
Numbers
z.number() // Basic number
z.number().int() // Integer only
z.number().positive() // > 0
z.number().nonnegative() // >= 0
z.number().negative() // < 0
z.number().nonpositive() // <= 0
z.number().min(0) // Minimum value
z.number().max(100) // Maximum value
z.number().gt(0) // Greater than
z.number().gte(0) // Greater than or equal
z.number().lt(100) // Less than
z.number().lte(100) // Less than or equal
z.number().multipleOf(5) // Must be multiple of 5
z.int() // Shorthand for z.number().int()
z.int32() // 32-bit integer
z.nan() // NaN value
Coercion (Type Conversion)
z.coerce.string() // Convert to string
z.coerce.number() // Convert to number
z.coerce.boolean() // Convert to boolean
z.coerce.bigint() // Convert to bigint
z.coerce.date() // Convert to Date
// Example: Parse query parameters
const QuerySchema = z.object({
page: z.coerce.number().int().positive(),
limit: z.coerce.number().int().max(100).default(10),
});
// "?page=5&limit=20" -> { page: 5, limit: 20 }
Other Primitives
z.boolean() // Boolean
z.date() // Date object
z.date().min(new Date("2020-01-01"))
z.date().max(new Date("2030-12-31"))
z.bigint() // BigInt
z.symbol() // Symbol
z.null() // Null
z.undefined() // Undefined
z.void() // Void (undefined)
Complex Types
Objects
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
address: z.object({
street: z.string(),
city: z.string(),
country: z.string(),
}),
});
type Person = z.infer<typeof PersonSchema>;
// Object methods
PersonSchema.shape // Access shape
PersonSchema.keyof() // Get union of keys
PersonSchema.extend({ role: z.string() }) // Add fields
PersonSchema.pick({ name: true }) // Pick specific fields
PersonSchema.omit({ age: true }) // Omit fields
PersonSchema.partial() // Make all fields optional
PersonSchema.required() // Make all fields required
PersonSchema.deepPartial() // Recursively optional
// Strict vs loose objects
z.strictObject({ ... }) // No extra keys allowed (throws)
z.object({ ... }) // Strips extra keys (default)
z.looseObject({ ... }) // Allows extra keys
Arrays
z.array(z.string()) // String array
z.array(z.number()).min(1) // At least 1 element
z.array(z.number()).max(10) // At most 10 elements
z.array(z.number()).length(5) // Exactly 5 elements
z.array(z.number()).nonempty() // At least 1 element
// Nested arrays
z.array(z.array(z.number())) // number[][]
Tuples
z.tuple([z.string(), z.number()]) // [string, number]
z.tuple([z.string(), z.number()]).rest(z.boolean()) // [string, number, ...boolean[]]
Enums and Literals
// Enum
const RoleEnum = z.enum(["admin", "user", "guest"]);
type Role = z.infer<typeof RoleEnum>; // "admin" | "user" | "guest"
// Literal values
z.literal("exact_value")
z.literal(42)
z.literal(true)
// Native TypeScript enum
enum Fruits {
Apple,
Banana,
}
z.nativeEnum(Fruits)
// Enum methods
RoleEnum.enum.admin // "admin"
RoleEnum.exclude(["guest"]) // Exclude values
RoleEnum.extract(["admin", "user"]) // Include only
Unions
// Basic union
z.union([z.string(), z.number()])
// Discriminated union (better performance & type inference)
const ResponseSchema = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.any() }),
z.object({ status: z.literal("error"), message: z.string() }),
]);
type Response = z.infer<typeof ResponseSchema>;
// { status: "success", data: any } | { status: "error", message: string }
Intersections
const BaseSchema = z.object({ id: z.string() });
const ExtendedSchema = z.object({ name: z.string() });
const Combined = z.intersection(BaseSchema, ExtendedSchema);
// Equivalent to: z.object({ id: z.string(), name: z.string() })
Records and Maps
// Record: object with typed keys and values
z.record(z.string()) // { [key: string]: string }
z.record(z.string(), z.number()) // { [key: string]: number }
// Partial record (some keys optional)
z.partialRecord(z.enum(["a", "b"]), z.string())
// Map
z.map(z.string(), z.number()) // Map<string, number>
z.set(z.string()) // Set<string>
Advanced Patterns
Load references/advanced-patterns.md for complete advanced validation and transformation patterns.
Quick Reference
Refinements (custom validation):
z.string().refine((val) => val.length >= 8, "Too short");
z.object({ password, confirmPassword }).superRefine((data, ctx) => { /* ... */ });
Transformations (modify data):
z.string().transform((val) => val.trim());
z.string().pipe(z.coerce.number());
Codecs (bidirectional transforms - NEW in v4.1):
const DateCodec = z.codec(
z.iso.datetime(),
z.date(),
{
decode: (str) => new Date(str),
encode: (date) => date.toISOString(),
}
);
Recursive Types:
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({ name: z.string(), subcategories: z.array(CategorySchema) })
);
Optional/Nullable:
z.string().optional() // string | undefined
z.string().nullable() // string | null
z.string().default("default") // Provides default if undefined
Readonly & Brand:
z.object({ ... }).readonly() // Readonly properties
z.string().brand<"UserId">() // Nominal typing
→ Load references/advanced-patterns.md for: Complete refinement patterns, async validation, codec examples, composable schemas, conditional validation, performance optimization
Error Handling
Load references/error-handling.md for complete error formatting and customization guide.
Quick Reference
Error Formatting Methods:
// For forms
const { fieldErrors } = z.flattenError(error);
// For nested data
const tree = z.treeifyError(error);
const nameError = tree.properties?.user?.properties?.name?.errors?.[0];
// For debugging
console.log(z.prettifyError(error));
Custom Error Messages (three levels):
// 1. Schema-level (highest priority)
z.string({ error: "Custom message" });
z.string().min(5, "Too short");
// 2. Per-parse level
schema.parse(data, { error: (issue) => ({ message: "..." }) });
// 3. Global level
z.config({ customError: (issue) => ({ message: "..." }) });
Localization (40+ languages):
z.config(z.locales.es()); // Spanish
z.config(z.locales.fr()); // French
→ Load references/error-handling.md for: Complete error formatting examples, custom error patterns, localization setup, error code reference
Type Inference
Load references/type-inference.md for complete type inference and metadata documentation.
Quick Reference
Basic Type Inference:
const UserSchema = z.object({ name: z.string() });
type User = z.infer<typeof UserSchema>; // { name: string }
Input vs Output (for transforms):
const TransformSchema = z.string().transform((s) => s.length);
type Input = z.input<typeof TransformSchema>; // string
type Output = z.output<typeof TransformSchema>; // number
JSON Schema Conversion:
const jsonSchema = z.toJSONSchema(UserSchema, {
target: "openapi-3.0",
metadata: true,
});
Metadata:
// Add metadata
const EmailSchema = z.string().email().meta({
title: "Email Address",
description: "User's email address",
});
// Create custom registry
const formRegistry = z.registry<FormFieldMeta>();
→ Load references/type-inference.md for: Complete type inference patterns, JSON Schema options, metadata system, custom registries, brand types
Functions
Validate function inputs and outputs:
const AddFunction = z.function()
.args(z.number(), z.number()) // Arguments
.returns(z.number()); // Return type
// Implement typed function
const add = AddFunction.implement((a, b) => {
return a + b; // Type-checked!
});
// Async functions
const FetchFunction = z.function()
.args(z.string())
.returns(z.promise(z.object({ data: z.any() })))
.implementAsync(async (url) => {
const response = await fetch(url);
return response.json();
});
Common Patterns
Environment Variables
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000),
API_KEY: z.string().min(32),
});
// Validate on startup
const env = EnvSchema.parse(process.env);
// Now use typed env
console.log(env.PORT); // number
API Request Validation
const CreateUserRequest = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(8),
age: z.number().int().positive().optional(),
});
// Express example
app.post("/users", async (req, res) => {
const result = CreateUserRequest.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: z.flattenError(result.error).fieldErrors,
});
}
const user = await createUser(result.data);
res.json(user);
});
Form Validation
const FormSchema = z.object({
firstName: z.string().min(1, "First name required"),
lastName: z.string().min(1, "Last name required"),
email: z.string().email("Invalid email"),
age: z.coerce.number().int().min(18, "Must be 18+"),
agreeToTerms: z.literal(true, {
errorMap: () => ({ message: "Must accept terms" }),
}),
});
type FormData = z.infer<typeof FormSchema>;
Partial Updates
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// For PATCH requests: make everything optional except id
const UpdateUserSchema = UserSchema.partial().required({ id: true });
type UpdateUser = z.infer<typeof UpdateUserSchema>;
// { id: string; name?: string; email?: string }
Composable Schemas
// Base schemas
const TimestampSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const AuthorSchema = z.object({
authorId: z.string(),
authorName: z.string(),
});
// Compose into larger schemas
const PostSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
}).merge(TimestampSchema).merge(AuthorSchema);
Ecosystem Integration
Load references/ecosystem-integrations.md for complete framework and tooling integration guide.
Quick Reference
ESLint Plugins:
eslint-plugin-zod-x- Enforces best practiceseslint-plugin-import-zod- Enforces import style
Framework Integrations:
- tRPC - End-to-end typesafe APIs
- React Hook Form - Form validation (see
react-hook-form-zodskill) - Prisma - Generate Zod from database models
- NestJS - DTOs and validation pipes
Code Generation:
- orval - OpenAPI → Zod
- Hey API - OpenAPI to TypeScript + Zod
- kubb - API toolkit with codegen
→ Load references/ecosystem-integrations.md for: Setup instructions, integration examples, Hono middleware, Drizzle ORM patterns
Troubleshooting
Load references/troubleshooting.md for complete troubleshooting guide, performance tips, and best practices.
Quick Reference
Common Issues:
- TypeScript strict mode required → Enable in
tsconfig.json - Large bundle size → Use
z.lazy()for code splitting - Slow async refinements → Cache or debounce
- Circular dependencies → Use
z.lazy() - Slow unions → Use
z.discriminatedUnion() - Transform vs refine confusion → Use
.refine()for validation,.transform()for modification
Performance Tips:
- Use
.discriminatedUnion()(5-10x faster than.union()) - Cache schema instances
- Use
.safeParse()(avoids try-catch overhead) - Lazy load large schemas
Best Practices:
- Define schemas at module level
- Use type inference (
z.infer) - Add custom error messages
- Validate at system boundaries
- Compose small schemas
- Document with
.meta()
→ Load references/troubleshooting.md for: Detailed solutions, performance optimization, best practices, testing patterns
Quick Reference
// Primitives
z.string(), z.number(), z.boolean(), z.date(), z.bigint()
// Collections
z.array(), z.tuple(), z.object(), z.record(), z.map(), z.set()
// Special types
z.enum(), z.union(), z.discriminatedUnion(), z.intersection()
z.literal(), z.any(), z.unknown(), z.never()
// Modifiers
.optional(), .nullable(), .nullish(), .default(), .catch()
.readonly(), .brand()
// Validation
.min(), .max(), .length(), .regex(), .email(), .url(), .uuid()
.refine(), .superRefine()
// Transformation
.transform(), .pipe(), .codec()
// Parsing
.parse(), .safeParse(), .parseAsync(), .safeParseAsync()
// Type inference
z.infer<typeof Schema>, z.input<typeof Schema>, z.output<typeof Schema>
// Error handling
z.flattenError(), z.treeifyError(), z.prettifyError()
// JSON Schema
z.toJSONSchema(schema, options)
// Metadata
.meta(), .describe()
// Object methods
.extend(), .pick(), .omit(), .partial(), .required(), .merge()
Zod v4 Internals (_zod Property)
Load references/v4-internals.md for complete internal structure documentation.
Quick Reference
Every Zod schema exposes _zod with the schema's internal definition:
const schema = z.string().min(3).max(50);
schema._zod.def.type; // "string"
schema._zod.def.checks; // Array of check schema instances
schema._zod.bag; // { minimum: 3, maximum: 50 }
schema._zod.version; // { major, minor, patch }
Schema-specific def fields:
- Object:
def.shape(Record of key to Zod schema instance) - Array:
def.element(inner Zod schema) - Union:
def.options(array of Zod schemas) - Optional/Nullable:
def.innerType(wrapped schema) - Literal:
def.values(array of primitive values) - Enum:
def.entries(object where keys === values) - Transform:
def.type === "pipe"withdef.out._zod.def.type === "transform" - Refine: Adds check with
check._zod.def.check === "custom"
String format schemas (z.email(), z.uuid(), etc.) put format info directly on _zod.def:
z.email()._zod.def.type; // "string"
z.email()._zod.def.check; // "string_format"
z.email()._zod.def.format; // "email"
z.email()._zod.def.pattern; // regex source string
Load references/v4-internals.md for: Complete _zod.def field reference, check structure details, _zod.bag metadata examples, transform/refine detection patterns, recursive traversal patterns, compilability checks
When to Load References
Load references/migration-guide.md when:
- Upgrading from Zod v3 to v4
- Questions about breaking changes
- Need migration checklist or rollback strategy
- Errors related to deprecated APIs (
.merge(),error.format(), etc.) - Number validation issues with
Infinityor unsafe integers
Load references/error-handling.md when:
- Need to format errors for forms or UI
- Implementing custom error messages
- Questions about
z.flattenError(),z.treeifyError(), orz.prettifyError() - Setting up localization for error messages
- Need error code reference or pattern examples
Load references/advanced-patterns.md when:
- Implementing custom refinements or async validation
- Need bidirectional transformations (codecs)
- Working with recursive types or self-referential data
- Questions about
.refine(),.transform(), or.codec() - Need performance optimization patterns
- Implementing conditional validation
Load references/type-inference.md when:
- Questions about TypeScript type inference
- Need to generate JSON Schema for OpenAPI or AI
- Implementing metadata system for forms or documentation
- Need custom registries for type-safe metadata
- Questions about
z.infer,z.input,z.output - Using brand types for ID safety
Load references/ecosystem-integrations.md when:
- Integrating with tRPC, React Hook Form, Prisma, or NestJS
- Setting up ESLint plugins for best practices
- Generating Zod schemas from OpenAPI (orval, Hey API, kubb)
- Questions about Hono middleware or Drizzle ORM
- Need framework-specific integration examples
Load references/troubleshooting.md when:
- Encountering TypeScript strict mode errors
- Bundle size concerns or lazy loading needs
- Performance issues with large unions or async refinements
- Questions about circular dependencies
- Need best practices or testing patterns
- Confusion between
.refine()and.transform()
Load references/v4-internals.md when:
- Building tools that programmatically inspect or extract Zod schemas
- Need to access
_zod.def,_zod.bag, or_zod.version - Implementing AOT compilers, code generators, or schema serializers
- Need to detect transforms, refines, or check compilability of schemas
- Working with schema definition objects (
_zod.def.type,_zod.def.checks) - Need to understand check structure (string checks, number checks, array checks)
- Working with string format schemas (
z.email(),z.uuid()) internal representation - Need recursive schema traversal patterns
- Understanding the difference between top-level format schemas and chained format checks
- Questions about
_zod.bagaggregated metadata
Additional Resources
- Official Docs: https://zod.dev
- GitHub: https://github.com/colinhacks/zod
- TypeScript Playground: https://zod-playground.vercel.app
- ESLint Plugin (Best Practices): https://github.com/JoshuaKGoldberg/eslint-plugin-zod-x
- tRPC Integration: https://trpc.io
- Ecosystem: https://zod.dev/ecosystem
Production Notes:
- Package version: 4.1.12+ (Zod 4.x stable)
- Zero dependencies
- Bundle size: 2kb (gzipped)
- TypeScript 5.5+ required
- Strict mode required
- Last verified: 2025-11-17
- Skill version: 2.0.0 (Updated with v4.1 enhancements)
What's New in This Version:
- ✨ Comprehensive v3 to v4 migration guide with breaking changes
- ✨ Enhanced error customization with three-level system
- ✨ Expanded metadata API with registry system
- ✨ Improved error formatting with practical examples
- ✨ Built-in localization support for 40+ locales
- ✨ Detailed codec documentation with real-world patterns
- ✨ Performance improvements and architectural changes explained
Signals
- GitHub stars
- 34
- Forks
- 4
- Last commit
- Sep 2026
ahel review
K1binfo
installs-packagesK1binfo
installs-packages (in references/quick-reference.md)
Automated review, not a security audit. Ruleset v1+k2.
Advanced
- Catalog kind
- skill
- Gateway key
zod-wakita181009- Source
- github.com/wakita181009/zod-aot