zod-4 — Claude Code skill: what it does, how to install
Provides validation patterns for Zod 4 and guides migration from Zod 3. It helps write schemas for forms, API payloads, and adapters.
npx skills add prowler-cloud/prowler --skill zod-4 -a claude-codeWhat zod-4 does
This skill guides coding agents through Zod 4 schema syntax and v3-to-v4 migrations. The library turned common string checks like z.email() and z.uuid() into top-level functions instead of chained methods. Calling legacy syntax such as z.string().email() will fail in Zod 4 projects.
The reference covers basic primitives, records, and discriminated unions. Error handling shifted as well. Zod 4 replaces the message argument with error, accepting a string or an (issue) => ... callback. In addition, the guide demonstrates data coercions via z.coerce and multi-field validation via superRefine.
It includes integration steps for react-hook-form using @hookform/resolvers/zod. Agents rely on these templates when deriving types with z.infer and parsing inputs using safeParse.
What it helps with
- Migrate Zod 3 schemas to Zod 4 syntax
- Validate React forms with
@hookform/resolvers/zod - Coerce request payload data types using
z.coerce - Run multi-field validation logic with
superRefine
Example prompts for the agent
- Migrate this user schema from Zod 3 to Zod 4, updating email and uuid validators
- Write a password confirmation schema using superRefine for react-hook-form
- Create a discriminated union schema for handling API success and error responses
How to install zod-4
1Skills CLI (Vercel)
npx skills add prowler-cloud/prowler --skill zod-4 -a claude-code2Manually: put the files in place
~/.claude/skills/zod-4/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/zod-4/
1Skills CLI (Vercel)
npx skills add prowler-cloud/prowler --skill zod-4 -a codex2Manually: put the files in place
.agents/skills/zod-4/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add prowler-cloud/prowler --skill zod-4 -a cursor2Manually: put the files in place
.cursor/skills/zod-4/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install prowler-cloud/prowler zod-42Manually: put the files in place
.github/skills/zod-4/SKILL.md1Skills CLI (Vercel)
npx skills add prowler-cloud/prowler --skill zod-4 -a gemini-cli2Platform CLI
gemini skills install https://github.com/prowler-cloud/prowler --path skills/zod-4Manually: put the files in place
.devin/skills/zod-4/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add prowler-cloud/prowler --skill zod-4 -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
From the same set4
The whole set →Similar
Questions about zod-4
How do I install zod-4 in Claude Code?
Run npx skills add prowler-cloud/prowler --skill zod-4 -a claude-code.
What does the zod-4 skill do?
It provides syntax patterns for Zod 4, covering top-level validators, error customization, and schema integration with react-hook-form.
What breaking changes from Zod 3 does zod-4 cover?
It highlights standalone functions like z.email(), replaces .nonempty() with .min(1), and switches error properties from message to error.
Source text
SKILL.md · Apache-2.0
---
name: zod-4
description: >
Zod 4 schema validation patterns.
Trigger: When creating or updating Zod v4 schemas for validation/parsing (forms, request payloads, adapters), including v3 -> v4 migration patterns.
license: Apache-2.0
metadata:
author: prowler-cloud
version: "1.0"
scope: [root, ui]
auto_invoke: "Creating Zod schemas"
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch, WebSearch, Task
---
## Breaking Changes from Zod 3
```typescript
// ❌ Zod 3 (OLD)
z.string().email()
z.string().uuid()
z.string().url()
z.string().nonempty()
z.object({ name: z.string() }).required_error("Required")
// ✅ Zod 4 (NEW)
z.email()
z.uuid()
z.url()
z.string().min(1)
z.object({ name: z.string() }, { error: "Required" })
```
## Basic Schemas
```typescript
import { z } from "zod";
// Primitives
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
// Top-level validators (Zod 4)
const emailSchema = z.email();
const uuidSchema = z.uuid();
const urlSchema = z.url();
// With constraints
const nameSchema = z.string().min(1).max(100);
const ageSchema = z.number().int().positive().max(150);
const priceSchema = z.number().min(0).multipleOf(0.01);
```
## Object Schemas
```typescript
const userSchema = z.object({
id: z.uuid(),
email: z.email({ error: "Invalid email address" }),
name: z.string().min(1, { error: "Name is required" }),
age: z.number().int().positive().optional(),
role: z.enum(["admin", "user", "guest"]),
metadata: z.record(z.string(), z.unknown()).optional(),
});
type User = z.infer<typeof userSchema>;
// Parsing
const user = userSchema.parse(data); // Throws on error
const result = userSchema.safeParse(data); // Returns { success, data/error }
if (result.success) {
console.log(result.data);
} else {
console.log(result.error.issues);
}
```
## Arrays and Records
```typescript
// Arrays
const tagsSchema = z.array(z.string()).min(1).max(10);
const numbersSchema = z.array(z.number()).nonempty();
// Records (objects with dynamic keys)
const scoresSchema = z.record(z.string(), z.number());
// { [key: string]: number }
// Tuples
const coordinatesSchema = z.tuple([z.number(), z.number()]);
// [number, number]
```
## Unions and Discriminated Unions
```typescript
// Simple union
const stringOrNumber = z.union([z.string(), z.number()]);
// Discriminated union (more efficient)
const resultSchema = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.unknown() }),
z.object({ status: z.literal("error"), error: z.string() }),
]);
```
## Transformations
```typescript
// Transform during parsing
const lowercaseEmail = z.email().transform(email => email.toLowerCase());
// Coercion (convert types)
const numberFromString = z.coerce.number(); // "42" → 42
const dateFromString = z.coerce.date(); // "2024-01-01" → Date
// Preprocessing
const trimmedString = z.preprocess(
val => typeof val === "string" ? val.trim() : val,
z.string()
);
```
## Refinements
```typescript
const passwordSchema = z.string()
.min(8)
.refine(val => /[A-Z]/.test(val), {
message: "Must contain uppercase letter",
})
.refine(val => /[0-9]/.test(val), {
message: "Must contain number",
});
// With superRefine for multiple errors
const formSchema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords don't match",
path: ["confirmPassword"],
});
}
});
```
## Optional and Nullable
```typescript
// Optional (T | undefined)
z.string().optional()
// Nullable (T | null)
z.string().nullable()
// Both (T | null | undefined)
z.string().nullish()
// Default values
z.string().default("unknown")
z.number().default(() => Math.random())
```
## Error Handling
```typescript
// Zod 4: Use 'error' param instead of 'message'
const schema = z.object({
name: z.string({ error: "Name must be a string" }),
email: z.email({ error: "Invalid email format" }),
age: z.number().min(18, { error: "Must be 18 or older" }),
});
// Custom error map
const customSchema = z.string({
error: (issue) => {
if (issue.code === "too_small") {
return "String is too short";
}
return "Invalid string";
},
});
```
## React Hook Form Integration
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const schema = z.object({
email: z.email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof schema>;
function Form() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
</form>
);
}
```
© prowler-cloud · Apache-2.0 · prowler-cloud/prowler