
Zod Validation
- 115 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
zod-validation is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- zod-validation
- AI & Agent Building
- AI-coding skill
Zod Validation by the numbers
- 115 all-time installs (skills.sh)
- +7 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #3,942 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill zod-validationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 115 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Zod Validation (v4)
Type-safe schema validation for TypeScript. Zod v4 introduces top-level format functions, z.stringbool(), z.iso.* date formats, z.overwrite() transforms, z.file(), z.toJSONSchema(), getter-based recursive types, .meta(), and the unified error function.
Package: zod (also zod/mini for smaller bundles)
Quick Reference
| Pattern | Usage |
|---|---|
z.object({ ... }) | Define object schemas |
z.string(), z.number() | Primitive types |
z.email(), z.url(), z.uuid() | Top-level string formats (v4) |
z.iso.date(), z.iso.datetime() | ISO date/time formats (v4) |
z.int(), z.float32(), z.int32() | Fixed-width number formats (v4) |
z.templateLiteral([...]) | Template literal type validation (v4) |
z.enum([...]), z.literal() | Enums and literals |
z.union([...]), z.discriminatedUnion() | Union types |
z.array(), z.tuple() | Array and tuple types |
z.record(), z.map() | Key-value collections |
.optional(), .nullable() | Optional and nullable modifiers |
.default(), .catch() | Default values and fallbacks |
.prefault() | Pre-parse default (parsed through schema, v4) |
z.coerce.number() | Type coercion before validation |
z.stringbool() | String-to-boolean parsing (v4) |
.transform(), .overwrite() | Output transforms |
z.pipe() | Chain schemas (output feeds next input) |
.refine(), .superRefine() | Custom validation logic |
.parse(), .safeParse() | Validate and extract data |
z.infer<typeof Schema> | Extract TypeScript type from schema |
z.input<>, z.output<> | Input vs output types with transforms |
z.file() | File instance validation (v4) |
z.toJSONSchema() | Convert schema to JSON Schema (v4) |
.meta(), .describe() | Attach metadata to schemas (v4) |
z.prettifyError() | Human-readable error formatting (v4) |
z.treeifyError() | Structured error tree (replaces .format(), v4) |
z.registry() | Typed schema registry (v4) |
{ error: (issue) => ... } | Unified error customization (v4) |
| Getter-based recursion | Recursive types without z.lazy() (v4) |
z.globalRegistry | Global schema metadata registry (v4) |
z.config(z.locales.en()) | Internationalized error messages (v4) |
Common Mistakes
| Mistake | Fix |
|---|---|
z.string().email() | z.email() (v4 top-level format) |
z.string().url() | z.url() (v4 top-level format) |
Using parse without try/catch | Use safeParse for error handling |
Forgetting .optional() | Add when field may be undefined |
z.any() for unknown data | Use z.unknown() for type-safe unknown |
.transform() for same-type changes | Use .overwrite() (introspectable, v4) |
Missing .min(1) on strings | Empty strings pass z.string() by default |
z.number().parse(Infinity) | z.number() rejects Infinity in v4 |
z.number().safe() for floats | .safe() equals .int() in v4 (no floats) |
z.lazy() for recursive types | Use getter syntax in v4 (retains object methods) |
required_error / invalid_type_error | Use unified error function in v4 |
.refine(fn, (val) => ({ message })) | Function-as-second-arg overload removed; use .superRefine() |
errorMap on schemas | Replaced by error function in v4 |
z.record(z.string()) single arg | v4 requires two args: z.record(z.string(), z.string()) |
.format() / .flatten() on ZodError | Deprecated; use z.treeifyError(err) in v4 |
.default() with input type value | v4 .default() uses output type; use .prefault() for input |
Delegation
Use this skill for Zod schema definition, validation, parsing, type inference, coercion, error formatting, metadata, and JSON Schema conversion. For form integration, delegate to the tanstack-form skill.
References
- Schema Types — primitives, string formats, number formats, template literals, objects, arrays, enums, unions, records, optional, nullable
- Transforms and Parsing — coercion, stringbool, transforms, overwrite, pipe, refinements, parsing, type inference
- Error Handling — unified error function, prettifyError, treeifyError, ZodError, error formatting, internationalization
- Common Patterns — form validation, API responses, environment variables, schema composition, recursive types, branded types
- Metadata and JSON Schema — meta, describe, globalRegistry, toJSONSchema, file validation, Zod Mini
Common Patterns
Form Validation
const LoginSchema = z.object({
email: z.email(),
password: z.string().min(8),
rememberMe: z.boolean().default(false),
});
const RegisterSchema = z
.object({
name: z.string().min(1).max(100),
email: z.email(),
password: z.string().min(8).max(100),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords must match',
path: ['confirmPassword'],
});API Response
const ApiResponse = <T extends z.ZodType>(dataSchema: T) =>
z.object({
data: dataSchema,
error: z.string().nullable(),
status: z.enum(['success', 'error']),
});
const UserResponse = ApiResponse(
z.object({
id: z.string(),
email: z.email(),
name: z.string(),
}),
);Paginated Response
const PaginatedResponse = <T extends z.ZodType>(itemSchema: T) =>
z.object({
items: z.array(itemSchema),
total: z.number().int().nonnegative(),
page: z.number().int().positive(),
pageSize: z.number().int().positive(),
hasNext: z.boolean(),
});Environment Variables
const EnvSchema = z.object({
DATABASE_URL: z.url(),
PORT: z.coerce.number().default(3000),
DEBUG: z.stringbool().default(false),
NODE_ENV: z.enum(['development', 'production', 'test']),
SESSION_SECRET: z.string().min(32),
API_KEY: z.string().optional(),
});
const env = EnvSchema.parse(process.env);Schema Composition
// Base schema shared across operations
const baseUserSchema = z.object({
name: z.string().min(1),
email: z.email(),
});
// Extend for creation (adds required fields)
const createUserSchema = baseUserSchema.extend({
password: z.string().min(8),
});
// Partial for updates (all fields optional)
const updateUserSchema = baseUserSchema.partial();
// Pick specific fields
const userEmailSchema = baseUserSchema.pick({ email: true });
// Merge two schemas
const fullProfileSchema = baseUserSchema.merge(
z.object({
bio: z.string().max(500).optional(),
avatar: z.url().optional(),
}),
);Recursive Types (v4 Getter Syntax)
v4 supports getter-based recursion that retains full object methods (.pick(), .partial(), .extend()):
// Single recursion
const Category = z.object({
name: z.string(),
get subcategories() {
return z.array(Category);
},
});
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }Mutually Recursive Types
const User = z.object({
email: z.email(),
get posts() {
return z.array(Post);
},
});
const Post = z.object({
title: z.string(),
get author() {
return User;
},
});
// Full object methods still work
Post.pick({ title: true });
Post.partial();
Post.extend({ publishDate: z.date() });Legacy Recursive Types (z.lazy)
Still supported but the getter syntax is preferred in v4:
type Category = {
name: string;
children: Category[];
};
const CategorySchema: z.ZodType<Category> = z.object({
name: z.string(),
children: z.lazy(() => z.array(CategorySchema)),
});Branded Types
const UserId = z.uuid().brand<'UserId'>();
const PostId = z.uuid().brand<'PostId'>();
type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;
function getUser(id: UserId) {
// Only accepts branded UserId, not plain string or PostId
}
const userId = UserId.parse('550e8400-e29b-41d4-a716-446655440000');
getUser(userId); // OKIntersection
const HasId = z.object({ id: z.string() });
const HasTimestamps = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
// Combine schemas with intersection
const Entity = z.intersection(HasId, HasTimestamps);
// Shorthand
const Entity2 = HasId.and(HasTimestamps);Preprocess
Transform input before schema validation:
const CommaSeparated = z.preprocess(
(val) => (typeof val === 'string' ? val.split(',') : val),
z.array(z.string()),
);
CommaSeparated.parse('a,b,c'); // ["a", "b", "c"]Error Handling
Unified Error Function (v4)
Zod v4 replaces errorMap, required_error, and invalid_type_error with a single error function:
// v3 (deprecated)
// z.string({
// required_error: "This field is required",
// invalid_type_error: "Not a string",
// });
// v4 — unified error function
z.string({
error: 'Must be a string',
});
// Dynamic error messages based on issue details
z.string({
error: (issue) => {
if (issue.input === undefined) {
return 'This field is required';
}
return 'Not a string';
},
});
// Conditional on issue code
z.string().min(5, {
error: (issue) => {
if (issue.code === 'too_small') {
return `Must be at least ${issue.minimum} characters`;
}
},
});ZodError Structure
const result = schema.safeParse(invalidData);
if (!result.success) {
const error = result.error; // ZodError instance
// Access individual issues
for (const issue of error.issues) {
console.log(issue.code); // Error code (e.g., "invalid_type", "too_small")
console.log(issue.path); // Path to the field (e.g., ["user", "email"])
console.log(issue.message); // Error message
}
}Common Issue Codes
| Code | When triggered |
|---|---|
invalid_type | Wrong type (expected string, got number) |
too_small | Below minimum length/value |
too_big | Above maximum length/value |
invalid_string | Failed string format check |
unrecognized_keys | Extra keys on strict objects |
custom | From .refine() / .superRefine() |
Pretty Printing Errors (v4)
const result = UserSchema.safeParse(invalidData);
if (!result.success) {
// Human-readable multi-line format
console.log(z.prettifyError(result.error));
// Output:
// ✖ Invalid input: expected string, received number
// → at username
// ✖ Too small: expected number to be >=0
// → at favoriteNumbers[1]
}Tree Error Formatting (v4)
z.treeifyError() replaces the deprecated .format() and .flatten() methods on ZodError:
const result = UserSchema.safeParse(invalidData);
if (!result.success) {
const tree = z.treeifyError(result.error);
// Structured error tree with nested field paths
}.format() and .flatten() still work but are deprecated in v4. Prefer z.treeifyError() for new code.
Field-Level Error Formatting
function formatZodErrors(error: z.ZodError): Record<string, string> {
const errors: Record<string, string> = {};
for (const issue of error.issues) {
const path = issue.path.join('.');
if (!errors[path]) {
errors[path] = issue.message;
}
}
return errors;
}
const result = RegisterSchema.safeParse(formData);
if (!result.success) {
const fieldErrors = formatZodErrors(result.error);
// { email: "Invalid email", confirmPassword: "Passwords must match" }
}Error Handling Patterns
Safe Parse with Early Return
function processUser(input: unknown) {
const result = UserSchema.safeParse(input);
if (!result.success) {
return { ok: false, errors: formatZodErrors(result.error) } as const;
}
return { ok: true, data: result.data } as const;
}Try-Catch with Parse
try {
const user = UserSchema.parse(input);
// user is fully typed
} catch (err) {
if (err instanceof z.ZodError) {
console.log(z.prettifyError(err));
}
throw err;
}Internationalization (v4)
Configure global locale for error messages:
import * as z from 'zod';
// Set English locale (default)
z.config(z.locales.en());
// Per-schema error messages remain possible via the error function
const AgeSchema = z.number().min(18, {
error: 'Must be 18 or older',
});Custom Error Map (v4)
Replace the global error generation with a custom function:
z.config({
customError: (issue) => {
if (issue.code === 'invalid_type') {
return `Expected ${issue.expected}, got ${typeof issue.input}`;
}
},
});Metadata and JSON Schema
Schema Metadata (.meta)
Attach arbitrary metadata to any schema:
const UserSchema = z.object({
firstName: z.string().describe('Your first name'),
lastName: z.string().meta({ title: 'last_name' }),
age: z.number().meta({ examples: [12, 99] }),
});.describe() sets a human-readable description. .meta() accepts an arbitrary key-value object for richer annotations.
Typed Registries (v4)
Create strongly-typed registries for associating schemas with metadata:
const myRegistry = z.registry<{ title: string; description: string }>();
const UserSchema = z.object({ name: z.string() });
myRegistry.add(UserSchema, {
title: 'User',
description: 'A user object',
});Typed registries provide type-safe metadata association. Use them for framework integrations that need to look up schema metadata at runtime.
Global Registry
Track schemas globally and associate metadata:
const EmailSchema = z.email();
z.globalRegistry.add(EmailSchema, {
id: 'email_address',
title: 'Email address',
description: 'Provide your email',
examples: ['naomie@example.com'],
});The global registry allows tools and frameworks to look up metadata associated with schemas at runtime.
JSON Schema Conversion (v4)
Convert Zod schemas to JSON Schema:
const UserSchema = z.object({
firstName: z.string().describe('Your first name'),
lastName: z.string().meta({ title: 'last_name' }),
age: z.number().meta({ examples: [12, 99] }),
});
const jsonSchema = z.toJSONSchema(UserSchema);Metadata from .describe() and .meta() is automatically included in the generated JSON Schema output.
Practical Use: OpenAPI / API Documentation
const CreateUserBody = z.object({
name: z.string().min(1).describe('Full name of the user'),
email: z.email().describe('Valid email address'),
role: z.enum(['admin', 'user']).default('user').describe('User role'),
});
// Convert for use in OpenAPI spec
const bodySchema = z.toJSONSchema(CreateUserBody);Zod Mini
A lighter variant for smaller bundle sizes:
import * as z from 'zod/mini';
const schema = z.boolean();
schema.parse(false);Zod Mini provides the same core validation API with a reduced footprint. Use it in client-side code where bundle size is critical. The full zod package includes additional utilities like .toJSONSchema(), .prettifyError(), and advanced features not available in the mini variant.
When to Use Zod Mini
| Use Case | Package |
|---|---|
| Client-side form validation | zod/mini |
| Server-side API validation | zod |
| JSON Schema generation | zod |
| Bundle-sensitive libraries | zod/mini |
| Full metadata / registry | zod |
Schema Types
Primitives
z.string();
z.number();
z.boolean();
z.bigint();
z.date();
z.symbol();
z.undefined();
z.null();
z.void();
z.any();
z.unknown();
z.never();String Formats (v4 Top-Level API)
Zod v4 uses top-level functions instead of method chaining:
z.email();
z.url();
z.uuid();
z.cuid();
z.cuid2();
z.ulid();
z.nanoid();
z.ipv4();
z.ipv6();
z.cidrv4(); // IP range (CIDR notation)
z.cidrv6();
z.base64();
z.base64url();
z.emoji();ISO Date/Time Formats
z.iso.date(); // "2024-01-15"
z.iso.time(); // "14:30:00"
z.iso.datetime(); // "2024-01-15T14:30:00Z"
z.iso.duration(); // "P3Y6M4DT12H30M5S"String Constraints
z.string().min(1); // Non-empty
z.string().max(100); // Max length
z.string().length(5); // Exact length
z.string().regex(/^[a-z]+$/); // Pattern
z.string().trim(); // Trim whitespace
z.string().toLowerCase(); // Lowercase
z.string().toUpperCase(); // Uppercase
z.string().startsWith('https');
z.string().endsWith('.com');
z.string().includes('@');Number Formats (v4)
Fixed-width numeric types with pre-configured min/max constraints:
z.int(); // Safe integer (Number.MIN_SAFE_INTEGER to MAX_SAFE_INTEGER)
z.int32(); // -2147483648 to 2147483647
z.uint32(); // 0 to 4294967295
z.float32(); // IEEE 754 32-bit float range
z.float64(); // IEEE 754 64-bit float range
z.int64(); // Returns ZodBigInt (exceeds safe number range)
z.uint64(); // Returns ZodBigInt (exceeds safe number range)Number Constraints
z.number().min(0);
z.number().max(100);
z.number().int(); // Safe integers only (v4: enforces safe range)
z.number().positive();
z.number().negative();
z.number().nonnegative();
z.number().nonpositive();
z.number().multipleOf(5);
z.number().finite();
z.number().safe(); // Same as .int() in v4In v4, z.number() no longer accepts Infinity. Both .safe() and .int() enforce the safe integer range.
Objects
const User = z.object({
id: z.string(),
email: z.email(),
age: z.number().optional(),
});
type User = z.infer<typeof User>;
// Modifiers
User.partial(); // All optional
User.required(); // All required
User.pick({ id: true, email: true });
User.omit({ age: true });
User.extend({ role: z.string() });
User.merge(OtherSchema);
User.passthrough(); // Allow extra keys
User.strict(); // Reject extra keys
User.strip(); // Remove extra keysArrays and Tuples
z.array(z.string());
z.array(z.number()).min(1); // Non-empty
z.array(z.number()).max(10);
z.array(z.number()).length(5);
z.array(z.number()).nonempty(); // Non-empty (typed)
// Tuple
z.tuple([z.string(), z.number()]);
z.tuple([z.string(), z.number()]).rest(z.boolean());Enums and Unions
// Native enum
z.enum(['admin', 'user', 'guest']);
// Union
z.union([z.string(), z.number()]);
z.string().or(z.number()); // Shorthand
// Discriminated union (better errors)
z.discriminatedUnion('type', [
z.object({ type: z.literal('email'), email: z.email() }),
z.object({ type: z.literal('phone'), phone: z.string() }),
]);
// Literal
z.literal('active');
z.literal(42);
z.literal(true);Records and Maps
// Record (v4 requires both key and value schemas)
z.record(z.string(), z.number()); // { [key: string]: number }
// Enum keys (v4: all keys required)
z.record(z.enum(['a', 'b']), z.number()); // { a: number, b: number }
z.partialRecord(z.enum(['a', 'b']), z.number()); // { a?: number, b?: number }
// Map
z.map(z.string(), z.number());Template Literal Types (v4)
Represent TypeScript template literal types with validation:
const greeting = z.templateLiteral(['hello, ', z.string()]);
// `hello, ${string}`
const cssUnits = z.enum(['px', 'em', 'rem', '%']);
const cssValue = z.templateLiteral([z.number(), cssUnits]);
// `${number}px` | `${number}em` | `${number}rem` | `${number}%`
const emailPattern = z.templateLiteral([
z.string().min(1),
'@',
z.string().max(64),
]);
// `${string}@${string}` (min/max constraints are enforced)Supports strings, string formats, numbers, booleans, bigints, enums, literals, and nested template literals. Constraints like .min() and .max() are enforced in the generated regex.
File Validation (v4)
Validate JavaScript File instances:
const fileSchema = z.file();
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.mime(['image/png', 'image/jpeg']); // MIME type
// Practical upload schema
const UploadSchema = z.object({
avatar: z.file().max(5_000_000).mime(['image/png', 'image/jpeg']),
document: z.file().max(10_000_000).mime(['application/pdf']),
});Optional, Nullable, and Defaults
z.string().optional(); // string | undefined
z.string().nullable(); // string | null
z.string().nullish(); // string | null | undefined
// Defaults
z.string().default('hello');
z.number().default(0);
// Catch (use default on parse error)
z.string().catch('fallback');Transforms and Parsing
Coercion
Coerce input to the target type before validation:
z.coerce.string(); // Converts to string
z.coerce.number(); // Converts to number
z.coerce.boolean(); // Falsy -> false, truthy -> true
z.coerce.date(); // Converts to Date
z.coerce.bigint(); // Converts to BigIntString to Boolean (v4)
// Env-style boolean parsing
z.stringbool();
// Recognized values:
// true: "true", "1", "yes", "on", "y", "enabled"
// false: "false", "0", "no", "off", "n", "disabled"
// Custom values
z.stringbool({
truthy: ['yes', 'true'],
falsy: ['no', 'false'],
});Transforms
// Transform output type (input -> different output)
z.string().transform((val) => val.length); // string -> number
z.string().transform((val) => parseInt(val, 10));
// Overwrite (same type, introspectable — v4)
z.number().overwrite((val) => val * 2);
z.string().overwrite((val) => val.trim());Use .overwrite() when the output type matches the input -- it preserves schema introspection. Use .transform() when the output type differs.
Default vs Prefault (v4)
In v4, .default() uses the output type and short-circuits parsing. Use .prefault() when you need the default to go through the parse pipeline:
// .default() — output type, skips parsing
const schema1 = z
.string()
.transform((val) => val.length)
.default(0);
schema1.parse(undefined); // 0
// .prefault() — input type, parsed through schema
const schema2 = z
.string()
.transform((val) => val.length)
.prefault('tuna');
schema2.parse(undefined); // 4Use .default() for most cases. Use .prefault() when transforms or refinements should apply to the default value.
Pipe
Chain schemas so the output of one becomes the input of the next:
// Coerce string to number, then validate
const stringToPositiveInt = z.pipe(
z.coerce.number(),
z.number().int().positive(),
);
stringToPositiveInt.parse('42'); // 42
stringToPositiveInt.parse('-1'); // throws
// Shorthand with .pipe() method
const trimmedEmail = z.string().pipe(z.email());Use z.pipe() when you need multi-step validation where each step expects a different input type.
Refinements
// Simple refinement
z.string().refine((val) => val.includes('@'), {
message: 'Must contain @',
});
// Multiple refinements
z.string()
.refine((val) => val.length > 0, 'Required')
.refine((val) => val.includes('@'), 'Must contain @');
// SuperRefine (custom error positioning)
z.string().superRefine((val, ctx) => {
if (!val.includes('@')) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must contain @',
});
}
});The .refine() overload that accepted a function as the second argument has been removed in v4. Use .superRefine() for dynamic error messages:
// v3 (removed in v4)
// z.string().refine(
// (val) => val.length > 10,
// (val) => ({ message: `${val} is not more than 10 characters` })
// );
// v4 replacement
z.string().superRefine((val, ctx) => {
if (val.length <= 10) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${val} is not more than 10 characters`,
});
}
});Cross-Field Validation
const PasswordForm = z
.object({
password: z.string().min(8),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: 'Passwords must match',
path: ['confirm'],
});Parsing
const schema = z.string();
// Throws on error
schema.parse('hello'); // 'hello'
schema.parse(123); // throws ZodError
// Returns result object (preferred)
schema.safeParse('hello'); // { success: true, data: 'hello' }
schema.safeParse(123); // { success: false, error: ZodError }
// Async (for async refinements/transforms)
await schema.parseAsync('hello');
await schema.safeParseAsync('hello');Type Inference
const UserSchema = z.object({
id: z.string(),
email: z.email(),
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string }
// Input vs Output types (when using transforms)
type UserInput = z.input<typeof UserSchema>;
type UserOutput = z.output<typeof UserSchema>;Use z.input<> and z.output<> when schemas include .transform() or .default() -- the input and output types will differ.
Transform Type Inference Example
const StringToNumberSchema = z.object({
count: z.string().transform((val) => parseInt(val, 10)),
name: z.string().default('anonymous'),
});
type Input = z.input<typeof StringToNumberSchema>;
// { count: string; name?: string }
type Output = z.output<typeof StringToNumberSchema>;
// { count: number; name: string }