Get the FREE Ultimate OpenClaw Setup Guide →

zod-v4

Scanned
npx machina-cli add skill BjornMelin/dev-skills/zod-v4 --openclaw
Files (1)
SKILL.md
6.9 KB

Zod 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

v3v4
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

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

  1. Step 1: pnpm add zod@^4.3.5
  2. Step 2: Define a schema, e.g. const User = z.object({ name: z.string().min(1), email: z.email(), age: z.number().positive() })
  3. 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

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers