zod-patterns
Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.
Best use case
zod-patterns is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.
Teams using zod-patterns should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/zod-patterns/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How zod-patterns Compares
| Feature / Agent | zod-patterns | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Zod schema validation, type-safe development, and strict TypeScript patterns. When user works with Zod, validates data, creates schemas, handles form validation, mentions z.object/z.string patterns, needs runtime validation, type-safe code, or strict TypeScript configuration.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Zod Patterns Agent
## What's New in Zod 4 (2025)
- **Performance**: `z.array()` 7.4x faster, `z.object()` 6.5x faster (compared to Zod v3)
- **Zod Mini**: Tree-shakable functional API variant
- **Metadata API**: Attach custom metadata to schemas
- **JSON Schema**: Built-in JSON Schema generation
- **Locales**: Built-in i18n support for error messages
- **`z.looseObject()`**: Replaces `.passthrough()`
- **`z.strictObject()`**: Replaces `.strict()`
## Installation
```bash
# Standard Zod v4 (recommended)
bun add zod
# Minimal bundle for tree-shaking (1.9KB gzipped)
bun add @zod/mini
```
Requires TypeScript 5.5+ with `"strict": true` in tsconfig.
### Using @zod/mini
```typescript
// Tree-shakable imports (only includes what you use)
import { z } from "@zod/mini";
// Same API as full Zod
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
});
// Results in significantly smaller bundles
// Full Zod: ~5KB | @zod/mini: ~1.9KB (tree-shaken)
```
For library authors targeting v4: `import { z } from "zod/v4/core";`
## Basic Usage
### Defining and Parsing
```typescript
import { z } from "zod";
// Define a schema
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
});
// Parse data (throws on invalid)
const user = UserSchema.parse({
name: "Alice",
email: "alice@example.com",
age: 25,
});
// Safe parse (returns result object)
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // typed as User
} else {
console.log(result.error.issues); // validation errors
}
```
### Type Inference
```typescript
// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
// { name: string; email: string; age: number }
// For schemas with transforms - input vs output types
type UserInput = z.input<typeof UserSchema>;
type UserOutput = z.output<typeof UserSchema>;
```
## Primitive Types
### Strings
```typescript
z.string(); // any string
z.string().min(1); // non-empty
z.string().max(100); // max length
z.string().length(5); // exact length
z.string().email(); // email format
z.string().url(); // URL format
z.string().uuid(); // UUID format
z.string().cuid(); // CUID format
z.string().cuid2(); // CUID2 format
z.string().ulid(); // ULID format
z.string().regex(/^[a-z]+$/); // custom regex
z.string().includes("@"); // contains substring
z.string().startsWith("http"); // starts with
z.string().endsWith(".com"); // ends with
z.string().datetime(); // ISO datetime
z.string().date(); // ISO date
z.string().time(); // ISO time
z.string().ip(); // IP address
z.string().trim(); // trim whitespace (transform)
z.string().toLowerCase(); // lowercase (transform)
z.string().toUpperCase(); // uppercase (transform)
```
### Numbers
```typescript
z.number(); // any 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(5); // >= 5
z.number().max(100); // <= 100
z.number().gt(5); // > 5
z.number().gte(5); // >= 5 (alias for min)
z.number().lt(100); // < 100
z.number().lte(100); // <= 100 (alias for max)
z.number().multipleOf(5); // divisible by 5
z.number().finite(); // not Infinity
z.number().safe(); // within safe integer range
```
### Other Primitives
```typescript
z.boolean(); // true or false
z.bigint(); // BigInt values
z.date(); // Date objects
z.undefined(); // undefined only
z.null(); // null only
z.void(); // undefined (for function returns)
z.any(); // bypass validation
z.unknown(); // any, but type-safe usage
z.never(); // always fails
```
## Coercion
Automatically convert input types before validation:
```typescript
// String coercion
z.coerce.string(); // String(input)
z.coerce.number(); // Number(input)
z.coerce.boolean(); // Boolean(input)
z.coerce.bigint(); // BigInt(input)
z.coerce.date(); // new Date(input)
// With validation
const ageSchema = z.coerce.number().int().positive();
ageSchema.parse("25"); // 25
ageSchema.parse("abc"); // throws - NaN is not positive
// Common pitfall: empty string becomes 0
z.coerce.number().parse(""); // 0 (might not be desired)
// Fix: preprocess to handle empty strings
const safeNumber = z.preprocess(
(val) => (val === "" ? undefined : val),
z.coerce.number(),
);
```
## Objects
### Basic Objects
```typescript
const PersonSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email().optional(),
});
// By default, unknown keys are stripped
PersonSchema.parse({ name: "Bob", age: 30, extra: "ignored" });
// { name: "Bob", age: 30 }
```
### Object Modes (Zod 4)
```typescript
// Standard - strips unknown keys
z.object({ name: z.string() });
// Loose - passes through unknown keys
z.looseObject({ name: z.string() });
// Strict - rejects unknown keys
z.strictObject({ name: z.string() });
```
### Object Manipulation
```typescript
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
password: z.string(),
createdAt: z.date(),
});
// Pick specific fields
const PublicUser = UserSchema.pick({ id: true, name: true, email: true });
// Omit sensitive fields
const SafeUser = UserSchema.omit({ password: true });
// Make all fields optional
const PartialUser = UserSchema.partial();
// Make specific fields optional
const UpdateUser = UserSchema.partial({ name: true, email: true });
// Make all fields required
const RequiredUser = UserSchema.required();
// Extend with additional fields
const AdminSchema = UserSchema.extend({
role: z.literal("admin"),
permissions: z.array(z.string()),
});
// Merge schemas (Zod 4 - use extend instead)
const Combined = BaseSchema.extend(ExtraSchema.shape);
```
### Nested Objects
```typescript
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zip: z.string().optional(),
});
const CompanySchema = z.object({
name: z.string(),
address: AddressSchema,
employees: z.array(
z.object({
name: z.string(),
department: z.string(),
}),
),
});
```
## Arrays and Collections
### Arrays
```typescript
z.array(z.string()); // string[]
z.array(z.string()).min(1); // at least 1 element
z.array(z.string()).max(10); // at most 10 elements
z.array(z.string()).length(5); // exactly 5 elements
z.array(z.string()).nonempty(); // same as .min(1)
// Access element schema
const arr = z.array(z.string());
arr.element; // z.string()
```
### Tuples
```typescript
// Fixed-length array with specific types
const PointSchema = z.tuple([z.number(), z.number()]);
type Point = z.infer<typeof PointSchema>; // [number, number]
// With rest elements
const ArgsSchema = z.tuple([z.string(), z.number()]).rest(z.boolean());
// [string, number, ...boolean[]]
```
### Records and Maps
```typescript
// Record<string, T>
z.record(z.string(), z.number()); // { [key: string]: number }
z.record(z.number()); // shorthand for string keys
// Map<K, V>
z.map(z.string(), z.number()); // Map<string, number>
// Set<T>
z.set(z.string()); // Set<string>
z.set(z.number()).min(1).max(10); // with size constraints
```
## Unions and Enums
### Unions
```typescript
// Basic union
const StringOrNumber = z.union([z.string(), z.number()]);
type SN = z.infer<typeof StringOrNumber>; // string | number
// Shorthand
const StringOrNumber2 = z.string().or(z.number());
```
### Discriminated Unions
```typescript
// More efficient parsing with discriminator key
const ResultSchema = z.discriminatedUnion("status", [
z.object({
status: z.literal("success"),
data: z.object({ id: z.string() }),
}),
z.object({
status: z.literal("error"),
message: z.string(),
}),
]);
type Result = z.infer<typeof ResultSchema>;
// { status: "success"; data: { id: string } } | { status: "error"; message: string }
// TypeScript narrows based on discriminator
const result = ResultSchema.parse(data);
if (result.status === "success") {
console.log(result.data.id); // typed correctly
}
```
### Enums
```typescript
// Zod enum (recommended)
const StatusSchema = z.enum(["pending", "active", "inactive"]);
type Status = z.infer<typeof StatusSchema>; // "pending" | "active" | "inactive"
// Access values
StatusSchema.options; // ["pending", "active", "inactive"]
StatusSchema.enum; // { pending: "pending", active: "active", ... }
// Extract or exclude values
StatusSchema.extract(["pending", "active"]); // only these
StatusSchema.exclude(["inactive"]); // all except these
// Native enum
enum NativeStatus {
Pending,
Active,
Inactive,
}
const NativeStatusSchema = z.nativeEnum(NativeStatus);
```
### Literals
```typescript
z.literal("hello"); // exactly "hello"
z.literal(42); // exactly 42
z.literal(true); // exactly true
z.null(); // exactly null
z.undefined(); // exactly undefined
```
## Optional, Nullable, Default
```typescript
// Optional - allows undefined
z.string().optional(); // string | undefined
// Nullable - allows null
z.string().nullable(); // string | null
// Nullish - allows null or undefined
z.string().nullish(); // string | null | undefined
// Default values
z.string().default("N/A"); // static default
z.number().default(() => Math.random()); // dynamic default
z.date().default(() => new Date()); // current date
// Catch - use default on any parse error
z.number().catch(0); // returns 0 on error
z.string().catch((ctx) => `Error: ${ctx.error.message}`);
// Unwrap optional/nullable
const optStr = z.string().optional();
optStr.unwrap(); // z.string()
```
## Refinements and Transforms
### Refinements (Custom Validation)
```typescript
// Basic refine
const PasswordSchema = z
.string()
.min(8, "Password must be at least 8 characters")
.refine((val) => /[A-Z]/.test(val), "Password must contain uppercase letter")
.refine((val) => /[0-9]/.test(val), "Password must contain number");
// With custom error path
const FormSchema = z
.object({
password: z.string(),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"], // error appears on this field
});
```
### SuperRefine (Multiple Issues)
```typescript
const ComplexSchema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
type: "string",
inclusive: true,
message: "Too short",
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Missing uppercase",
});
}
});
```
### Transforms
```typescript
// Transform output type
const TrimmedString = z.string().transform((val) => val.trim());
const NumberFromString = z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int());
const DateFromISO = z
.string()
.datetime()
.transform((val) => new Date(val));
// Transform with validation
const PositiveFromString = z
.string()
.transform((val, ctx) => {
const num = parseInt(val, 10);
if (isNaN(num)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Not a number",
});
return z.NEVER;
}
return num;
})
.pipe(z.number().positive());
```
### Preprocess
```typescript
// Transform BEFORE parsing
const TrimmedEmail = z.preprocess(
(val) => (typeof val === "string" ? val.trim().toLowerCase() : val),
z.string().email(),
);
// Handle empty strings
const OptionalNumber = z.preprocess(
(val) => (val === "" ? undefined : val),
z.coerce.number().optional(),
);
```
### Pipe (Chaining Schemas)
```typescript
// Chain multiple schemas
const ParsedInt = z
.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().int().positive());
// With intermediate validation
const EmailDomain = z
.string()
.email()
.pipe(z.string().endsWith("@company.com"));
```
## Error Handling
### Custom Error Messages
```typescript
// Per-validator messages
z.string().min(5, "Must be at least 5 characters");
z.string().min(5, { message: "Too short" });
// Schema-level errors
z.string("Expected a string");
z.string({
error: (issue) =>
issue.input === undefined ? "Field is required" : "Must be a string",
});
```
### Parsing Errors
```typescript
const result = UserSchema.safeParse(input);
if (!result.success) {
// Formatted errors
const formatted = result.error.format();
// { name: { _errors: ["Required"] }, email: { _errors: ["Invalid email"] } }
// Flattened errors
const flat = result.error.flatten();
// { formErrors: [], fieldErrors: { name: ["Required"], email: ["Invalid email"] } }
// Raw issues
result.error.issues.forEach((issue) => {
console.log(issue.path, issue.message, issue.code);
});
}
```
### Error Maps
```typescript
// Per-parse error customization
schema.parse(data, {
error: (issue) => `Custom error: ${issue.code}`,
});
// Global configuration
z.config({
customError: (issue) => {
if (issue.code === "invalid_type") {
return `Expected ${issue.expected}, got ${issue.received}`;
}
return issue.message;
},
});
// Localization
import { en } from "zod/v4/locales/en";
z.config(en());
```
## Advanced Patterns
### Branded Types
```typescript
const UserId = z.string().uuid().brand<"UserId">();
const PostId = z.string().uuid().brand<"PostId">();
type UserId = z.infer<typeof UserId>;
type PostId = z.infer<typeof PostId>;
function getUser(id: UserId) {
/* ... */
}
const userId = UserId.parse("550e8400-e29b-41d4-a716-446655440000");
const postId = PostId.parse("550e8400-e29b-41d4-a716-446655440000");
getUser(userId); // OK
getUser(postId); // Type error!
```
### Recursive Types
```typescript
interface Category {
name: string;
subcategories: Category[];
}
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema),
}),
);
```
### JSON Type
```typescript
// Generic JSON value
const JsonValue: z.ZodType<unknown> = z.lazy(() =>
z.union([
z.string(),
z.number(),
z.boolean(),
z.null(),
z.array(JsonValue),
z.record(JsonValue),
]),
);
```
### Function Schemas
```typescript
const FnSchema = z.function().args(z.string(), z.number()).returns(z.boolean());
type Fn = z.infer<typeof FnSchema>;
// (args_0: string, args_1: number) => boolean
const validated = FnSchema.implement((name, age) => {
return age > 18;
});
```
### Async Validation
```typescript
const UniqueEmailSchema = z
.string()
.email()
.refine(async (email) => {
const exists = await checkEmailExists(email);
return !exists;
}, "Email already taken");
// Must use async parse
const result = await UniqueEmailSchema.safeParseAsync("test@example.com");
```
## Integration Patterns
### Form Validation
```typescript
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
const FormSchema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "At least 8 characters"),
});
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(FormSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
{/* ... */}
</form>
);
}
```
### API Validation
```typescript
// Hono example
import { zValidator } from "@hono/zod-validator";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
app.post("/users", zValidator("json", CreateUserSchema), async (c) => {
const data = c.req.valid("json");
// data is fully typed
});
```
### Environment Variables
```typescript
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
API_KEY: z.string().min(1),
});
export const env = EnvSchema.parse(process.env);
```
## Replacing Common Type Checks with Zod
Prefer Zod validation over `typeof`, `instanceof`, or type guard functions.
### typeof / instanceof / Number checks → Zod
```typescript
// ❌ Avoid typeof operator
if (typeof value === "string") {
return value.toUpperCase();
}
// ✅ Prefer Zod validation
const result = z.string().safeParse(value);
if (result.success) {
return result.data.toUpperCase();
}
// ❌ Avoid instanceof
if (err instanceof Error) {
console.log(err.message);
}
// ✅ Prefer Zod
const result = z.instanceof(Error).safeParse(err);
if (result.success) {
console.log(result.data.message);
}
// ❌ Avoid Number.isInteger
if (Number.isInteger(value)) {
return value * 2;
}
// ✅ Prefer Zod
const result = z.number().int().safeParse(value);
if (result.success) {
return result.data * 2;
}
```
### Type predicates → Zod
```typescript
// ❌ Avoid type guard functions
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "email" in value;
}
// ✅ Prefer Zod schema validation
const UserSchema = z.object({
email: z.string().email(),
name: z.string(),
});
const result = UserSchema.safeParse(value);
if (result.success) {
const user = result.data; // Type-safe!
}
```
## Type Assertions Rules
Only `as unknown` and `as const` are allowed — all other `as` casts are banned.
```typescript
// ❌ Never do this - bypasses type safety
const user = data as User;
const id = value as string;
// ✅ Cast to unknown first, then validate
const data = response as unknown;
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data;
}
// ✅ Use 'as const' for literal types
const STATUSES = ["pending", "approved", "rejected"] as const;
type Status = (typeof STATUSES)[number];
```
**Why no type assertions?** They bypass TypeScript's type checking, don't perform runtime validation, can cause runtime crashes, and hide bugs instead of catching them. Zod gives you both compile-time AND runtime safety.
## Strict TypeScript Configuration
### tsconfig.json Best Practices
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}
```
### Use 'type' Instead of 'interface'
```typescript
// ✅ Prefer type
type User = {
id: string;
email: string;
};
type Admin = User & {
permissions: string[];
};
// ❌ Avoid interface
interface User {
id: string;
email: string;
}
```
**Why?** Types are more flexible (unions, intersections) and consistent with Zod's inferred types.
## Best Practices Summary
1. **Use `safeParse()`** - avoid throwing in production code
2. **Infer types** - let Zod generate TypeScript types
3. **Compose schemas** - use `extend`, `pick`, `omit` for reuse
4. **Prefer coercion** over preprocess for type conversion
5. **Add custom messages** - improve user-facing errors
6. **Use discriminated unions** - for efficient tagged unions
7. **Brand sensitive types** - prevent accidental ID swapping
8. **Validate at boundaries** - API inputs, env vars, external data
9. **Use transforms sparingly** - keep schemas focused on validation
10. **Test edge cases** - empty strings, null, undefined
## When to Ask for Help
- Complex recursive schema patterns
- Performance optimization for high-throughput validation
- Integration with specific ORMs or frameworks
- Migration from other validation libraries
- Custom error formatting requirements
- Schema generation from external sourcesRelated Skills
argocd-app-patterns
Use when asking about ArgoCD applications, Helm chart deployment via ArgoCD, sync policies, or typed Helm values patterns.
toolkit-recall
Search past decisions, prior research, conversation history, monorepo docs, and fetched web pages. Use toolkit recall search to find context from previous work, and toolkit fetch to save web pages for future search.
zellij-helper
Zellij terminal multiplexer for session management, layouts, and pane operations When user mentions Zellij, terminal multiplexer, zellij commands, sessions, panes, layouts, or tabs
worktree-workflow
Git worktree workflow for isolated feature development and PR creation When user starts new work, needs to switch contexts, or wants parallel development
vite-react-helper
Vite + React for fast modern web development - build config, HMR, hooks, state management, and performance patterns When user works with Vite, React, creates components, manages state, uses hooks, or configures Vite builds
version-management
Use when asking about version management, Renovate annotations, versions.ts patterns, or pinning image/chart versions.
typst-authoring
This skill should be used when the user asks to "write Typst", "create a Typst document", "format in Typst", "convert to Typst", "Typst syntax", "Typst template", "Typst math", "Typst table", or works with .typ files. Provides comprehensive Typst markup, scripting, math, layout, and styling reference for authoring documents. Also use proactively when generating .typ output files (e.g., in deep-research reports).
typescript-helper
TypeScript development guidance for type systems and tooling When user works with .ts or .tsx files, mentions TypeScript, or encounters type errors
torvalds-deployment
Use when asking about adding services, createXxxDeployment patterns, or homelab deployment conventions. Services use per-namespace charts.
terraform-helper
Terraform and OpenTofu infrastructure as code - HCL, providers, modules, state management, and CLI operations When user works with .tf files, mentions Terraform, OpenTofu, tofu, HCL, infrastructure as code, or tf commands
terminal-concepts
Comprehensive guide for building CLI and TUI applications - terminal internals, design principles, and battle-tested patterns When building CLI/TUI apps, implementing argument parsing, handling terminal input/output, escape codes, buffering, signals, or asking about terminal development concepts
talos-helper
Talos Linux cluster administration using talosctl When user mentions Talos, talosctl, or Talos cluster operations