home / skills / prowler-cloud / prowler / zod-4

zod-4 skill

/skills/zod-4

This skill helps you migrate and validate Zod v3 to v4 schemas, applying new patterns for parsing, validation, and error handling.

npx playbooks add skill prowler-cloud/prowler --skill zod-4

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
4.9 KB
---
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>
  );
}
```

Overview

This skill provides concise Zod v4 schema validation patterns and migration guidance from Zod v3. It focuses on modern idioms for primitives, objects, arrays, unions, transformations, refinements, and error handling. Use it when creating or updating validation for forms, request payloads, adapters, or libraries. The content highlights breaking changes, common pitfalls, and integration tips for frameworks like React Hook Form.

How this skill works

The skill inspects schema definitions and suggests idiomatic Zod v4 constructs: top-level validators (z.email(), z.uuid(), z.url()), object options with { error }, coercion and preprocess flows, and the newer error mapping APIs. It also outlines migration patterns from Zod v3 to v4, such as replacing z.string().email() with z.email() and required_error -> { error }. Examples show parsing (parse/safeParse), transformations, refinements, and discriminated unions for efficient branching.

When to use it

  • When migrating existing Zod v3 schemas to Zod v4 to avoid breaking changes.
  • When validating API request payloads or responses in back-end services.
  • When building form validation in front-end apps (React Hook Form integration).
  • When you need type-safe parsing with coercion or preprocessing of incoming values.
  • When implementing complex validation logic using refinements or superRefine.

Best practices

  • Use top-level validators for common types: z.email(), z.uuid(), z.url() to simplify schemas.
  • Prefer safeParse for non-throwing validation in production flows and parse when you want exceptions.
  • Use discriminatedUnion for performance and clearer error paths on tagged unions.
  • Keep user-facing text in { error } or custom error maps; avoid embedding UI language in logic.
  • Use z.coerce and z.preprocess at input boundaries to normalize types before validation.

Example use cases

  • Validate incoming REST or GraphQL payloads with object schemas and safeParse.
  • Form validation in React using zodResolver and z.email(), z.string().min() rules.
  • Coerce query string parameters (numbers, dates) into typed values with z.coerce.* or z.preprocess.
  • Implement multi-field checks (password/confirm) with superRefine to add per-field issues.
  • Define dynamic record shapes (metadata or scores) with z.record(keySchema, valueSchema).

FAQ

How do I convert z.string().email() from Zod 3 to Zod 4?

Replace it with z.email() as a top-level validator. Apply .transform or .refine afterwards if you need additional behavior.

How do I set custom error text in Zod 4?

Pass { error: 'Your message' } to top-level validators or constraint calls (e.g., z.string().min(1, { error: 'Required' })). For programmatic control, use a custom error map function.