zod-v4
Scannednpx machina-cli add skill BjornMelin/dev-skills/zod-v4 --openclawZod v4 Schema Validation
Quick Start
pnpm add zod@^4.3.5
import { z } from 'zod';
// Define schema
const User = z.object({
name: z.string().min(1),
email: z.email(),
age: z.number().positive(),
});
// Parse (throws on error)
const user = User.parse({ name: "Alice", email: "alice@example.com", age: 30 });
// Safe parse (returns result)
const result = User.safeParse(data);
if (result.success) {
result.data; // validated
} else {
console.log(z.prettifyError(result.error));
}
// Type inference
type User = z.infer<typeof User>;
Versioning + Imports (v4.3.5)
- Use
import { z } from "zod"for v4 (package root now exports v4). - Use
import * as z from "zod/mini"for Zod Mini. - Use
import * as z from "zod/v3"only if you must stay on v3.
Workflow: Determine Task Type
Designing new schemas? → Read API Reference
Migrating from Zod 3? → Read Migration Guide
Working with codecs, errors, JSON Schema, or metadata? → Read Advanced Features
Integrating with frameworks (RHF, tRPC, Hono, Next.js)? → Read Ecosystem Patterns
Key v4 Concepts
Top-Level String Formats
v4 moved string validators to top-level functions:
// v4 style (preferred)
z.email()
z.uuid()
z.url()
z.ipv4()
z.ipv6()
z.iso.date()
z.iso.datetime()
// v3 style (deprecated but works)
z.string().email()
Object Variants
z.object({}) // Allows unknown keys (default)
z.strictObject({}) // Rejects unknown keys
z.looseObject({}) // Explicitly allows unknown keys
Unified Error Parameter
// String message
z.string().min(5, { error: "Too short" });
// Function for dynamic messages
z.string({
error: (iss) => iss.input === undefined ? "Required" : "Invalid"
});
Type Inference
const Schema = z.object({ name: z.string() });
type Schema = z.infer<typeof Schema>;
// For transforms, get input/output separately
const Transformed = z.string().transform(s => s.length);
type Input = z.input<typeof Transformed>; // string
type Output = z.output<typeof Transformed>; // number
Common Patterns
Discriminated Unions
const Event = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("keypress"), key: z.string() }),
]);
Exhaustive Records
const Status = z.enum(["pending", "active", "done"]);
// All keys required
z.record(Status, z.number()) // { pending: number; active: number; done: number }
// Keys optional
z.partialRecord(Status, z.number()) // { pending?: number; active?: number; done?: number }
Recursive Schemas
const Category = z.object({
name: z.string(),
get subcategories() { return z.array(Category) }
});
Branded Types
const UserId = z.string().brand<"UserId">();
const PostId = z.string().brand<"PostId">();
type UserId = z.infer<typeof UserId>;
// Cannot assign UserId to PostId
Transforms and Pipes
// Transform
z.string().transform(s => s.toUpperCase())
// Pipe (chain schemas)
z.pipe(
z.string(),
z.coerce.number(),
z.number().positive()
)
Default Values
// Output default (v4)
z.string().default("guest")
// Input default (pre-transform)
z.string().transform(s => s.toUpperCase()).prefault("hello")
// Missing => "HELLO"
Error Handling
Pretty Print
const result = schema.safeParse(data);
if (!result.success) {
console.log(z.prettifyError(result.error));
// ✖ Invalid email
// → at email
}
Flat Structure (Forms)
const flat = z.flattenError(result.error);
// { formErrors: [], fieldErrors: { email: ["Invalid email"] } }
Tree Structure (Nested)
const tree = z.treeifyError(result.error);
// { properties: { email: { errors: ["Invalid email"] } } }
JSON Schema / OpenAPI
const schema = z.object({
name: z.string(),
email: z.email(),
}).meta({ id: "User", title: "User" });
// Generate JSON Schema
const jsonSchema = z.toJSONSchema(schema);
// For OpenAPI 3.0
z.toJSONSchema(schema, { target: "openapi-3.0" });
// Using registry for multiple schemas
z.globalRegistry.add(schema, schema.meta());
const allSchemas = z.toJSONSchema(z.globalRegistry);
v3 to v4 Migration Quick Reference
| v3 | v4 |
|---|---|
z.string().email() | z.email() |
z.nativeEnum(MyEnum) | z.enum(MyEnum) |
{ message: "..." } | { error: "..." } |
.strict() | z.strictObject({}) |
.passthrough() | z.looseObject({}) |
.merge(other) | .extend(other.shape) |
z.record(valueSchema) | z.record(z.string(), valueSchema) |
.deepPartial() | Nest .partial() manually |
error.format() | z.treeifyError(error) |
error.flatten() | z.flattenError(error) |
Breaking Changes
- Numbers: No
Infinity, stricter.safe()and.int() - UUID: RFC 4122 compliant (use
z.guid()for permissive) - Defaults in optional:
z.string().default("x").optional()now applies default - z.unknown(): No longer implicitly optional
- Error precedence: Schema-level wins over global
Run codemod: npx zod-v3-to-v4
Framework Integration Quick Start
React Hook Form
import { zodResolver } from '@hookform/resolvers/zod';
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(schema),
});
tRPC
publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => getById(input.id))
Hono
import { zValidator } from '@hono/zod-validator';
app.post('/users', zValidator('json', schema), (c) => {
const data = c.req.valid('json');
});
Next.js Server Actions
'use server';
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) {
return { errors: z.flattenError(result.error).fieldErrors };
}
Reference Files
- API Reference - All schema types, methods, and validation APIs
- Advanced Features - Codecs, error handling, metadata, JSON Schema
- Migration Guide - Complete v3 to v4 migration reference
- Ecosystem Patterns - Framework integrations and organization patterns
Source
git clone https://github.com/BjornMelin/dev-skills/blob/main/skills/zod-v4/SKILL.mdView on GitHub Overview
Zod v4 Schema Validation provides expert guidance for designing, validating, and migrating Zod v4 schemas in TypeScript. It covers core APIs, error handling, and ecosystem patterns for RHF, tRPC, Hono, and Next.js, plus advanced topics like JSON Schema/OpenAPI generation, codecs/transforms, and recursive or branded types.
How This Skill Works
The skill compiles practical examples and patterns from the SKILL.md, including a Quick Start, Versioning and Imports, and a task-oriented Workflow. It explains core concepts such as top-level string formats, object variants, unified error parameters, and type inference, then demonstrates how to apply them in real-world integrations and with recursive/branded schemas.
When to Use It
- Designing new schemas in TypeScript with Zod v4
- Migrating from Zod 3 to Zod v4
- Handling validation errors and generating JSON Schema/OpenAPI
- Working with codecs, transforms, and type inference
- Integrating with React Hook Form, tRPC, Hono, or Next.js
Quick Start
- Step 1: pnpm add zod@^4.3.5
- Step 2: Define a schema, e.g. const User = z.object({ name: z.string().min(1), email: z.email(), age: z.number().positive() })
- Step 3: Parse and validate, e.g. const user = User.parse({ name: 'Alice', email: 'alice@example.com', age: 30 }); const result = User.safeParse(data); if (result.success) { /* use result.data */ } else { console.log(z.prettifyError(result.error)); } type User = z.infer<typeof User>;
Best Practices
- Use top-level string validators (z.email, z.uuid, z.url, etc.) for clarity and consistency
- Choose the right object variant (z.object, z.strictObject, or z.looseObject) based on whether unknown keys are allowed
- Leverage the unified error parameter to customize messages for different validation failures
- Exploit type inference with z.infer, z.input, and z.output to manage types around transforms
- Use branded types and recursive schemas to enforce strong, scalable typing across large schemas
Example Use Cases
- Define a User schema with name, email, and age, then parse and safely parse data, using prettifyError to log issues
- Create a discriminated union for events (e.g., click vs. keypress) with type-based branches
- Implement branded IDs like UserId and PostId to prevent cross-type assignment
- Demonstrate transforms and pipes, e.g., transforming strings and composing schemas with z.pipe
- Show output vs input defaults, using z.string().default and pre-transform defaults for user-friendly inputs