Back to skills
SkillHub ClubShip Full StackFull StackBackend

zod

Zod v4 best practices, patterns, and API guidance for schema validation, parsing, error handling, and type inference in TypeScript applications. Covers safeParse, object composition, refinements, transforms, codecs, branded types, and v3→v4 migration. Baseline: zod ^4.0.0. Triggers on: zod imports, z.object, z.string, z.infer, safeParse, mentions of "zod", "schema validation", "zod v4", or "z.enum".

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,131
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-zod-skill

Repository

openclaw/skills

Skill path: skills/anivar/zod-skill

Zod v4 best practices, patterns, and API guidance for schema validation, parsing, error handling, and type inference in TypeScript applications. Covers safeParse, object composition, refinements, transforms, codecs, branded types, and v3→v4 migration. Baseline: zod ^4.0.0. Triggers on: zod imports, z.object, z.string, z.infer, safeParse, mentions of "zod", "schema validation", "zod v4", or "z.enum".

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install zod into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding zod to shared team environments
  • Use zod for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: zod
description: >
  Zod v4 best practices, patterns, and API guidance for schema validation,
  parsing, error handling, and type inference in TypeScript applications.
  Covers safeParse, object composition, refinements, transforms, codecs,
  branded types, and v3→v4 migration. Baseline: zod ^4.0.0.
  Triggers on: zod imports, z.object, z.string, z.infer, safeParse,
  mentions of "zod", "schema validation", "zod v4", or "z.enum".
license: MIT
user-invocable: false
agentic: false
compatibility: "TypeScript ^5.5 projects using zod ^4.0.0"
metadata:
  author: Anivar Aravind
  author_url: https://anivar.net
  source_url: https://github.com/anivar/zod-skill
  version: 1.0.0
  tags: zod, validation, schema, typescript, parsing, type-inference, forms, api
---

# Zod

**IMPORTANT:** Your training data about `zod` may be outdated or incorrect — Zod v4 introduces breaking changes to string formats, enums, error handling, and recursive types. Always rely on this skill's rule files and the project's actual source code as the source of truth. Do not fall back on memorized v3 patterns when they conflict with the retrieved reference.

## When to Use Zod

Zod is for **runtime type validation** — parsing untrusted data at system boundaries (API input, form data, env vars, external services). For compile-time-only types, plain TypeScript is sufficient.

| Need | Recommended Tool |
|------|-----------------|
| API input/output validation | **Zod** |
| Form validation (React, Vue) | **Zod** (with react-hook-form, formik, etc.) |
| Env var parsing | **Zod** (with t3-env or manual) |
| Compile-time types only | Plain TypeScript |
| Smaller bundle (~1kb) | Valibot |
| Maximum type inference | ArkType |

## Rule Categories by Priority

| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Parsing & Type Safety | CRITICAL | `parse-` |
| 2 | Schema Design | CRITICAL | `schema-` |
| 3 | Refinements & Transforms | HIGH | `refine-` |
| 4 | Error Handling | HIGH | `error-` |
| 5 | Performance & Composition | MEDIUM | `perf-` |
| 6 | v4 Migration | MEDIUM | `migrate-` |
| 7 | Advanced Patterns | MEDIUM | `pattern-` |
| 8 | Architecture & Boundaries | CRITICAL/HIGH | `arch-` |
| 9 | Observability | HIGH/MEDIUM | `observe-` |

## Quick Reference

### 1. Parsing & Type Safety (CRITICAL)

- `parse-use-safeParse` — Use `safeParse()` for user input instead of `parse()` which throws
- `parse-async-required` — Must use `parseAsync()`/`safeParseAsync()` when schema has async refinements
- `parse-infer-types` — Use `z.infer<typeof Schema>` for output types; never manually duplicate types

### 2. Schema Design (CRITICAL)

- `schema-object-unknowns` — `z.object()` strips unknown keys; use `strictObject` or `looseObject`
- `schema-union-discriminated` — Use `z.discriminatedUnion()` for tagged unions, not `z.union()`
- `schema-coercion-pitfalls` — `z.coerce.boolean()` makes `"false"` → `true`; use `z.stringbool()`
- `schema-recursive-types` — Use getter pattern for recursive schemas; `z.lazy()` is removed in v4

### 3. Refinements & Transforms (HIGH)

- `refine-never-throw` — Never throw inside `.refine()` or `.transform()`; use `ctx.addIssue()`
- `refine-vs-transform` — `.refine()` for validation, `.transform()` for conversion, `.pipe()` for staging
- `refine-cross-field` — `.superRefine()` on parent object for cross-field validation with `path`

### 4. Error Handling (HIGH)

- `error-custom-messages` — Use v4 `error` parameter; `required_error`/`invalid_type_error` are removed
- `error-formatting` — `z.flattenError()` for forms, `z.treeifyError()` for nested; `formatError` deprecated
- `error-input-security` — Never use `reportInput: true` in production; leaks sensitive data

### 5. Performance & Composition (MEDIUM)

- `perf-extend-spread` — Use `{ ...Schema.shape }` spread over chained `.extend()` for large schemas
- `perf-reuse-schemas` — Define once, derive with `.pick()`, `.omit()`, `.partial()`
- `perf-zod-mini` — Use `zod/v4/mini` (1.88kb) for bundle-critical client apps

### 6. v4 Migration (MEDIUM)

- `migrate-string-formats` — Use `z.email()`, `z.uuid()`, `z.url()` not `z.string().email()`
- `migrate-native-enum` — Use unified `z.enum()` for TS enums; `z.nativeEnum()` is removed
- `migrate-error-api` — Use `error` parameter everywhere; `message`, `errorMap` are removed

### 7. Advanced Patterns (MEDIUM)

- `pattern-branded-types` — `.brand<"Name">()` for nominal typing (USD vs EUR)
- `pattern-codecs` — `z.codec()` for bidirectional transforms (parse + serialize)
- `pattern-pipe` — `.pipe()` for staged parsing (string → number → validate range)

### 8. Architecture & Boundaries (CRITICAL/HIGH)

- `arch-boundary-parsing` — Parse at system boundaries (API handler, env, form, fetch); pass typed data to domain logic
- `arch-schema-organization` — Co-locate schemas with their boundary layer; domain types use `z.infer`
- `arch-schema-versioning` — Additive changes only for non-breaking evolution; new fields use `.optional()`

### 9. Observability (HIGH/MEDIUM)

- `observe-structured-errors` — Use `z.flattenError()` for compact structured logs with request correlation IDs
- `observe-error-metrics` — `trackedSafeParse()` wrapper to increment counters per schema and field on failure

## Schema Types Quick Reference

| Type | Syntax |
|------|--------|
| String | `z.string()` |
| Number | `z.number()`, `z.int()`, `z.float()` |
| Boolean | `z.boolean()` |
| BigInt | `z.bigint()` |
| Date | `z.date()` |
| Undefined | `z.undefined()` |
| Null | `z.null()` |
| Void | `z.void()` |
| Any | `z.any()` |
| Unknown | `z.unknown()` |
| Never | `z.never()` |
| Literal | `z.literal("foo")`, `z.literal(42)` |
| Enum | `z.enum(["a", "b"])`, `z.enum(TSEnum)` |
| Email | `z.email()` |
| URL | `z.url()` |
| UUID | `z.uuid()` |
| String→Bool | `z.stringbool()` |
| ISO DateTime | `z.iso.datetime()` |
| File | `z.file()` |
| JSON | `z.json()` |
| Array | `z.array(schema)` |
| Tuple | `z.tuple([a, b])` |
| Object | `z.object({})` |
| Strict Object | `z.strictObject({})` |
| Loose Object | `z.looseObject({})` |
| Record | `z.record(keySchema, valueSchema)` |
| Map | `z.map(keySchema, valueSchema)` |
| Set | `z.set(schema)` |
| Union | `z.union([a, b])` |
| Disc. Union | `z.discriminatedUnion("key", [...])` |
| Intersection | `z.intersection(a, b)` |

## How to Use

Read individual rule files for detailed explanations and code examples:

```
rules/parse-use-safeParse.md
rules/schema-object-unknowns.md
```

Each rule file contains:

- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and decision tables

## References

| Priority | Reference | When to read |
|----------|-----------|-------------|
| 1 | `references/schema-types.md` | All primitives, string formats, numbers, enums, dates |
| 2 | `references/parsing-and-inference.md` | parse, safeParse, z.infer, coercion |
| 3 | `references/objects-and-composition.md` | Objects, arrays, unions, pick/omit/partial, recursive |
| 4 | `references/refinements-and-transforms.md` | refine, superRefine, transform, pipe, defaults |
| 5 | `references/error-handling.md` | ZodError, flattenError, treeifyError, error customization |
| 6 | `references/advanced-features.md` | Codecs, branded types, JSON Schema, registries |
| 7 | `references/anti-patterns.md` | Common mistakes with BAD/GOOD examples |
| 8 | `references/boundary-architecture.md` | Where Zod fits: Express, tRPC, Next.js, React Hook Form, env, external APIs |
| 9 | `references/linter-and-ci.md` | ESLint rules, CI schema snapshots, unused schema detection, circular deps |

## Full Compiled Document

For the complete guide with all rules expanded: `AGENTS.md`


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### rules/parse-use-safeParse.md

```markdown
---
title: Use safeParse() for User Input
impact: CRITICAL
description: Use safeParse() instead of parse() to avoid throwing on invalid input.
tags: parsing, safeParse, error-handling, validation
---

# Use safeParse() for User Input

## Problem

`parse()` throws a `ZodError` on invalid input. Wrapping it in try/catch is verbose and error-prone — you lose the discriminated success/error result and risk catching unrelated errors.

## Incorrect

```typescript
// BUG: try/catch is verbose and catches unrelated errors
function validateInput(data: unknown) {
  try {
    const result = UserSchema.parse(data)
    return { success: true, data: result }
  } catch (e) {
    if (e instanceof z.ZodError) {
      return { success: false, errors: e.errors }
    }
    throw e // rethrow non-Zod errors
  }
}
```

## Correct

```typescript
function validateInput(data: unknown) {
  const result = UserSchema.safeParse(data)
  if (!result.success) {
    return { success: false, errors: result.error.issues }
  }
  return { success: true, data: result.data }
}
```

## Why

`safeParse()` returns a discriminated union `{ success: true, data } | { success: false, error }`. No exceptions, no try/catch, no risk of catching unrelated errors. Use `parse()` only when invalid data is truly exceptional (e.g., internal config that should never be wrong).

```

### references/schema-types.md

```markdown
# Schema Types Reference

## Primitives

```typescript
z.string()     // string
z.number()     // number (int or float)
z.boolean()    // boolean
z.bigint()     // bigint
z.date()       // Date instance
z.symbol()     // symbol
z.undefined()  // undefined
z.null()       // null
z.void()       // void (undefined)
z.any()        // any — bypasses type checking
z.unknown()    // unknown — safer than any
z.never()      // never — always fails
```

## Strings

### Constraints

```typescript
z.string().min(5)           // minimum length
z.string().max(100)         // maximum length
z.string().length(10)       // exact length
z.string().regex(/^[a-z]+$/) // regex pattern
z.string().trim()           // trims whitespace (transform)
z.string().toLowerCase()    // lowercases (transform)
z.string().toUpperCase()    // uppercases (transform)
z.string().startsWith("https://")
z.string().endsWith(".com")
z.string().includes("@")
```

### Top-Level String Formats (v4)

```typescript
z.email()       // email address
z.url()         // URL
z.uuid()        // UUID (v4)
z.cuid()        // CUID
z.cuid2()       // CUID2
z.ulid()        // ULID
z.emoji()       // emoji character
z.nanoid()      // Nano ID
z.ipv4()        // IPv4 address
z.ipv6()        // IPv6 address
z.cidrv4()      // CIDR v4 notation
z.cidrv6()      // CIDR v6 notation
z.jwt()         // JSON Web Token
z.base64()      // Base64 string
z.base64url()   // Base64url string
```

### ISO Date/Time Strings

```typescript
z.iso.date()      // "2024-01-15"
z.iso.time()      // "13:45:30"
z.iso.datetime()  // "2024-01-15T13:45:30Z"
z.iso.duration()  // "P3Y6M4DT12H30M5S"
```

### Template Literals

```typescript
// Validates strings matching a template pattern
const UserID = z.templateLiteral([z.literal("user_"), z.string()])
// Matches: "user_abc123", "user_xyz"
// Rejects: "abc123", "admin_xyz"
```

## Numbers

```typescript
z.number()                // any number
z.int()                   // integer only
z.float()                 // float (alias for number)

// Constraints
z.number().min(0)         // >= 0
z.number().max(100)       // <= 100
z.number().positive()     // > 0
z.number().negative()     // < 0
z.number().nonnegative()  // >= 0
z.number().nonpositive()  // <= 0
z.number().multipleOf(5)  // divisible by 5
z.number().finite()       // not Infinity
z.number().safe()         // within Number.MAX_SAFE_INTEGER
```

## BigInt

```typescript
z.bigint()
z.bigint().min(0n)
z.bigint().max(100n)
z.bigint().positive()
z.bigint().negative()
z.bigint().nonnegative()
z.bigint().nonpositive()
z.bigint().multipleOf(5n)
```

## Boolean

```typescript
z.boolean()        // true or false
z.literal(true)    // only true
z.literal(false)   // only false
```

## Date

```typescript
z.date()                           // Date instance
z.date().min(new Date("2020-01-01")) // after date
z.date().max(new Date("2030-01-01")) // before date
```

## Enums

```typescript
// String literal array
z.enum(["active", "inactive", "pending"])

// TypeScript enum (v4 — unified, no more nativeEnum)
enum Status {
  Active = "active",
  Inactive = "inactive",
}
z.enum(Status)

// Numeric enum
enum Priority {
  Low = 0,
  Medium = 1,
  High = 2,
}
z.enum(Priority)
```

## Stringbool

Converts string boolean representations to actual booleans.

```typescript
z.stringbool()
// Accepts: "true"/"false", "1"/"0", "yes"/"no", "on"/"off"
// Returns: boolean

z.stringbool().parse("true")   // true
z.stringbool().parse("false")  // false
z.stringbool().parse("1")      // true
z.stringbool().parse("0")      // false
z.stringbool().parse("yes")    // true
z.stringbool().parse("no")     // false
```

## Literals

```typescript
z.literal("hello")   // exactly "hello"
z.literal(42)        // exactly 42
z.literal(true)      // exactly true
z.literal(null)      // exactly null
z.literal(undefined) // exactly undefined
z.literal(100n)      // exactly 100n (bigint)
```

## Files

```typescript
z.file()                         // File instance
z.file().min(1024)               // minimum size in bytes
z.file().max(5 * 1024 * 1024)    // maximum size (5MB)
z.file().type("image/png")       // MIME type
z.file().type("image/*")         // MIME type wildcard
```

## JSON

```typescript
// Validates that the input is a valid JSON string, then parses it
z.json()

z.json().parse('{"name":"Alice"}')  // { name: "Alice" }
z.json().parse("invalid json")     // ZodError
```

## Custom Types

```typescript
// Custom type with validation function
const NonEmptyString = z.custom<string>(
  (val) => typeof val === "string" && val.length > 0,
  { error: "Must be a non-empty string" }
)
```

## Optional, Nullable, Nullish

```typescript
z.string().optional()   // string | undefined
z.string().nullable()   // string | null
z.string().nullish()    // string | null | undefined
```

## Coercion

```typescript
z.coerce.string()   // String(input)
z.coerce.number()   // Number(input)
z.coerce.boolean()  // Boolean(input) — careful: Boolean("false") === true
z.coerce.bigint()   // BigInt(input)
z.coerce.date()     // new Date(input)
```

```

### references/parsing-and-inference.md

```markdown
# Parsing and Type Inference Reference

## Parsing Methods

### parse(data)

Parses input and returns the validated data. Throws `ZodError` on failure.

```typescript
const result = UserSchema.parse(data) // returns User or throws
```

Use when invalid data is truly exceptional (internal config, constants).

### safeParse(data)

Returns a discriminated union — never throws.

```typescript
const result = UserSchema.safeParse(data)
if (result.success) {
  result.data // typed as User
} else {
  result.error // ZodError
}
```

Preferred for all user input, API boundaries, form data.

### parseAsync(data)

Required when schema contains async refinements or transforms. Throws on failure.

```typescript
const result = await UserSchema.parseAsync(data)
```

### safeParseAsync(data)

Async version of safeParse. Required for async refinements/transforms.

```typescript
const result = await UserSchema.safeParseAsync(data)
if (result.success) {
  result.data
} else {
  result.error
}
```

### When to Use Which

| Method | Throws | Async | Use When |
|--------|--------|-------|----------|
| `parse()` | Yes | No | Internal data, config — invalid = bug |
| `safeParse()` | No | No | User input, API — invalid = expected |
| `parseAsync()` | Yes | Yes | Async refinements, internal data |
| `safeParseAsync()` | No | Yes | Async refinements, user input |

## Type Inference

### z.infer<typeof Schema>

Extracts the **output** type (after transforms).

```typescript
const UserSchema = z.object({
  name: z.string(),
  createdAt: z.string().transform((s) => new Date(s)),
})

type User = z.infer<typeof UserSchema>
// { name: string; createdAt: Date }
```

### z.input<typeof Schema>

Extracts the **input** type (before transforms).

```typescript
type UserInput = z.input<typeof UserSchema>
// { name: string; createdAt: string }
```

Useful for form state, request bodies, or any context where you work with pre-transform data.

### z.output<typeof Schema>

Alias for `z.infer`. Extracts the output type.

```typescript
type User = z.output<typeof UserSchema>
// Same as z.infer<typeof UserSchema>
```

### Input vs Output Examples

```typescript
const FormSchema = z.object({
  age: z.string().pipe(z.coerce.number()),        // string → number
  active: z.stringbool(),                          // string → boolean
  date: z.string().transform((s) => new Date(s)), // string → Date
})

type FormInput = z.input<typeof FormSchema>
// { age: string; active: string; date: string }

type FormOutput = z.infer<typeof FormSchema>
// { age: number; active: boolean; date: Date }
```

## Coercion Namespace

Coercion schemas apply JavaScript constructors before validation.

```typescript
z.coerce.string()   // String(input) → then validate as string
z.coerce.number()   // Number(input) → then validate as number
z.coerce.boolean()  // Boolean(input) → then validate as boolean
z.coerce.bigint()   // BigInt(input) → then validate as bigint
z.coerce.date()     // new Date(input) → then validate as date
```

### Coercion Pitfall: Boolean

```typescript
z.coerce.boolean().parse("false") // true — Boolean("false") is true
z.coerce.boolean().parse("")      // false — Boolean("") is false
z.coerce.boolean().parse("0")     // true — Boolean("0") is true

// Use z.stringbool() for string→boolean from forms/env vars
z.stringbool().parse("false") // false
z.stringbool().parse("0")     // false
```

## Parse Options

```typescript
// reportInput — includes raw input in error issues
schema.safeParse(data, { reportInput: true })
// error.issues[0].input will contain the raw value

// Only use in development — leaks sensitive data in production
schema.safeParse(data, {
  reportInput: process.env.NODE_ENV === "development",
})
```

```

### references/objects-and-composition.md

```markdown
# Objects and Composition Reference

## Object Variants

### z.object() — Strips Unknown Keys

```typescript
const User = z.object({
  name: z.string(),
  email: z.email(),
})

User.parse({ name: "Alice", email: "[email protected]", extra: true })
// { name: "Alice", email: "[email protected]" } — extra is stripped
```

### z.strictObject() — Rejects Unknown Keys

```typescript
const Config = z.strictObject({
  host: z.string(),
  port: z.number(),
})

Config.parse({ host: "localhost", port: 3000, debug: true })
// ZodError: Unrecognized key "debug"
```

### z.looseObject() — Preserves Unknown Keys

```typescript
const Proxy = z.looseObject({
  id: z.string(),
})

Proxy.parse({ id: "123", extra: true, nested: { a: 1 } })
// { id: "123", extra: true, nested: { a: 1 } }
```

## Object Methods

### .shape

Access the raw shape object for spreading.

```typescript
const User = z.object({ name: z.string(), email: z.email() })
User.shape // { name: ZodString, email: ZodEmail }

// Use for spreading
const Extended = z.object({ ...User.shape, age: z.number() })
```

### .keyof()

Returns a `z.enum()` of the object's keys.

```typescript
const UserKey = User.keyof()
// z.enum(["name", "email"])

UserKey.parse("name")  // "name"
UserKey.parse("age")   // ZodError
```

### .extend()

Add new fields to an object schema.

```typescript
const WithAge = User.extend({ age: z.number() })
```

### .safeExtend()

Extend with compile-time error on conflicting keys.

```typescript
const WithAge = User.safeExtend({ age: z.number() })
// TypeScript error if "age" already exists in User
```

### .pick()

Select specific fields.

```typescript
const NameOnly = User.pick({ name: true })
// z.object({ name: z.string() })
```

### .omit()

Remove specific fields.

```typescript
const NoPassword = User.omit({ password: true })
```

### .partial()

Make all fields optional.

```typescript
const PartialUser = User.partial()
// { name?: string; email?: string }

// Partial specific fields
const PartialName = User.partial({ name: true })
// { name?: string; email: string }
```

### .required()

Make all fields required.

```typescript
const RequiredUser = PartialUser.required()
```

### .catchall(schema)

Validate unknown keys against a schema.

```typescript
const Config = z.object({ host: z.string() }).catchall(z.string())
// Known keys validated by their schemas, unknown keys must be strings
```

## Recursive Objects

Use the getter pattern (v4 — `z.lazy()` is removed).

```typescript
const Category = z.object({
  name: z.string(),
  get children() {
    return z.array(Category).optional()
  },
})

type Category = z.infer<typeof Category>
// { name: string; children?: Category[] | undefined }
```

## 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() // at least 1, narrows type to [string, ...string[]]
```

## Tuples

```typescript
// Fixed-length typed array
z.tuple([z.string(), z.number(), z.boolean()])
// [string, number, boolean]

// With rest element
z.tuple([z.string(), z.number()]).rest(z.boolean())
// [string, number, ...boolean[]]
```

## Records

```typescript
// Dictionary with string keys
z.record(z.string(), z.number())
// Record<string, number>

// Enum keys
z.record(z.enum(["a", "b"]), z.number())
// { a: number; b: number }
```

### z.partialRecord()

Values can be undefined.

```typescript
z.partialRecord(z.string(), z.number())
// Record<string, number | undefined>
```

### z.looseRecord()

Preserves extra keys.

```typescript
z.looseRecord(z.string(), z.number())
```

## Maps and Sets

```typescript
z.map(z.string(), z.number())       // Map<string, number>
z.set(z.string())                    // Set<string>
z.set(z.string()).min(1)             // at least 1 element
z.set(z.string()).max(10)            // at most 10 elements
z.set(z.string()).nonempty()         // non-empty set
```

## Unions

### z.union()

Sequential matching — tries each branch in order.

```typescript
z.union([z.string(), z.number()])
// string | number
```

### z.discriminatedUnion()

O(1) dispatch on a shared discriminator field.

```typescript
z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), value: z.string() }),
  z.object({ type: z.literal("b"), count: z.number() }),
])
```

### z.xor()

Exactly one must match.

```typescript
z.xor(
  z.object({ email: z.email() }),
  z.object({ phone: z.string() }),
)
// Must have email OR phone, not both
```

## Intersection

```typescript
z.intersection(
  z.object({ name: z.string() }),
  z.object({ age: z.number() }),
)
// { name: string; age: number }
```

Prefer `.extend()` or spread over intersection for object merging — intersection has edge cases with overlapping keys.

```

### references/refinements-and-transforms.md

```markdown
# Refinements and Transforms Reference

## .refine(fn, opts)

Custom validation that returns a boolean.

```typescript
const EvenNumber = z.number().refine((n) => n % 2 === 0, {
  error: "Must be even",
})

// Async refinement — must use parseAsync/safeParseAsync
const UniqueEmail = z.email().refine(
  async (email) => !(await db.exists(email)),
  { error: "Email taken" }
)
```

## .superRefine((val, ctx) => void)

Advanced validation with multiple issues and path targeting.

```typescript
const Password = z.string().superRefine((val, ctx) => {
  if (val.length < 8) {
    ctx.addIssue({
      code: "custom",
      message: "Must be at least 8 characters",
    })
  }
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({
      code: "custom",
      message: "Must contain uppercase letter",
    })
  }
  if (!/[0-9]/.test(val)) {
    ctx.addIssue({
      code: "custom",
      message: "Must contain a number",
    })
  }
})
```

### Cross-Field Validation

```typescript
const Form = z
  .object({
    password: z.string().min(8),
    confirm: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirm) {
      ctx.addIssue({
        code: "custom",
        path: ["confirm"], // targets the confirm field
        message: "Passwords don't match",
      })
    }
  })
```

## .check(...checks)

Functional-style checks (also available in full Zod, required in Zod Mini).

```typescript
// Full Zod
z.string().check(
  z.minLength(8, "Too short"),
  z.maxLength(100, "Too long"),
)

// Zod Mini
import { z } from "zod/v4/mini"
z.string().check(z.minLength(8), z.maxLength(100))
```

## .transform(fn)

Converts the value to a new type (one-way).

```typescript
const StringToNumber = z.string().transform((s) => parseInt(s, 10))
// Input: string, Output: number

const Trimmed = z.string().transform((s) => s.trim())
// Input: string, Output: string (trimmed)
```

Never throw inside transforms — use `.refine()` first for validation.

## .pipe(schema)

Staged parsing — output of current schema becomes input of next.

```typescript
const PortNumber = z
  .string()
  .pipe(z.coerce.number())
  .pipe(z.int().min(1).max(65535))

// Stage 1: validate string
// Stage 2: coerce to number
// Stage 3: validate integer in range
```

## .preprocess(fn, schema)

Transform input before parsing. Legacy — prefer `.pipe()`.

```typescript
const TrimmedString = z.preprocess(
  (val) => (typeof val === "string" ? val.trim() : val),
  z.string().min(1)
)
```

## .overwrite(fn)

Modify value in place without changing type.

```typescript
const NormalizedEmail = z.email().overwrite((email) => email.toLowerCase())
// Input: string, Output: string (lowercased)
```

## .default(value)

Provides default for undefined input. Applied after validation.

```typescript
const Port = z.number().default(3000)
Port.parse(undefined) // 3000
Port.parse(8080)      // 8080
```

Input type becomes optional:
```typescript
type Input = z.input<typeof Port>   // number | undefined
type Output = z.infer<typeof Port>  // number
```

## .prefault(value)

Provides default before validation.

```typescript
const Name = z.string().min(1).prefault("Anonymous")
Name.parse(undefined) // "Anonymous" (validated against min(1) first)
```

## .catch(value)

Fallback on any error — never fails.

```typescript
const SafeNumber = z.number().catch(0)
SafeNumber.parse("not a number") // 0
SafeNumber.parse(42)             // 42
```

## .apply(fn)

Apply a function that returns a schema.

```typescript
const Clamped = z.number().apply((schema) =>
  schema.min(0).max(100)
)
```

## Async Refinements and Transforms

When any refinement or transform is async, you must use `parseAsync()` or `safeParseAsync()`.

```typescript
const Schema = z.object({
  email: z.email().refine(
    async (email) => !(await db.exists(email)),
    { error: "Taken" }
  ),
  avatar: z.url().transform(
    async (url) => await downloadImage(url)
  ),
})

// REQUIRED: async parse
const result = await Schema.safeParseAsync(data)
```

## Issue Codes for ctx.addIssue()

| Code | Use When |
|------|----------|
| `"custom"` | Custom validation logic |
| `"invalid_type"` | Wrong input type |
| `"too_small"` | Below minimum (length, value, size) |
| `"too_big"` | Above maximum |
| `"invalid_string"` | String format validation |
| `"invalid_enum_value"` | Value not in enum |
| `"unrecognized_keys"` | Unknown keys in strict object |
| `"invalid_union"` | No union branch matched |

```

### references/error-handling.md

```markdown
# Error Handling Reference

## ZodError

```typescript
const result = schema.safeParse(data)
if (!result.success) {
  result.error           // ZodError instance
  result.error.issues    // array of ZodIssue
  result.error.message   // JSON string of issues
  result.error.toString() // same as .message
}
```

## Issue Structure

```typescript
interface ZodIssue {
  code: string        // issue type code
  message: string     // human-readable message
  path: (string | number)[]  // path to the field
  input?: unknown     // raw input (only if reportInput: true)
  // ... additional fields depending on code
}
```

## Issue Codes

| Code | When | Extra Fields |
|------|------|-------------|
| `invalid_type` | Wrong type | `expected`, `received` |
| `too_small` | Below minimum | `minimum`, `inclusive`, `type` |
| `too_big` | Above maximum | `maximum`, `inclusive`, `type` |
| `invalid_string` | String format failure | `validation` |
| `custom` | Custom refinement | — |
| `invalid_enum_value` | Not in enum | `options`, `received` |
| `unrecognized_keys` | Unknown keys (strict) | `keys` |
| `invalid_union` | No branch matched | `unionErrors` |
| `invalid_arguments` | Function args invalid | `argumentsError` |
| `invalid_return_type` | Function return invalid | `returnTypeError` |

## Error Formatting Functions

### z.flattenError(error)

Flat structure for simple forms.

```typescript
const flat = z.flattenError(result.error)
// {
//   formErrors: ["Root-level error"],
//   fieldErrors: {
//     email: ["Invalid email"],
//     age: ["Must be positive", "Must be integer"],
//   }
// }
```

### z.treeifyError(error)

Nested tree matching schema shape. For deeply nested forms.

```typescript
const tree = z.treeifyError(result.error)
// {
//   errors: [],
//   properties: {
//     address: {
//       errors: [],
//       properties: {
//         zip: { errors: ["Required"] }
//       }
//     }
//   }
// }
```

### z.prettifyError(error)

Human-readable string for logging/debugging.

```typescript
const pretty = z.prettifyError(result.error)
// "✖ Invalid email at «email»
//  ✖ Required at «address.zip»"
```

### z.formatError() — Deprecated

Do not use. Removed in v4. Use `flattenError` or `treeifyError` instead.

## Error Customization

### Schema Level

```typescript
// String shorthand
z.string({ error: "Must be a string" })
z.number().min(18, { error: "Must be 18+" })
z.number().min(18, "Must be 18+") // shorthand

// Function form — dynamic messages
z.string({
  error: (issue) => {
    if (issue.input === undefined) return "Required"
    return "Must be a string"
  },
})
```

### Parse Level

```typescript
schema.safeParse(data, {
  error: (issue) => {
    // Override error for this parse call only
    return `Validation failed: ${issue.code}`
  },
})
```

### Global Level

```typescript
z.config({
  customError: (issue) => {
    // Global default error messages
    if (issue.code === "invalid_type") {
      return `Expected ${issue.expected}, got ${issue.received}`
    }
  },
})
```

## Error Precedence

1. **Schema-level** `error` — highest priority
2. **Parse-level** `error` — middle priority
3. **Global** `z.config({ customError })` — lowest priority

If a schema has an `error` parameter, it always wins over parse-level and global settings.

## reportInput Option

Includes raw input values in error issues. **Never use in production.**

```typescript
// Development only
const result = schema.safeParse(data, { reportInput: true })
if (!result.success) {
  result.error.issues[0].input // contains the raw value
}
```

Leaks passwords, tokens, PII into logs and error monitoring.

## i18n / Localization

Use the error function form for localized messages.

```typescript
const t = getTranslation(locale)

const NameSchema = z.string({
  error: (issue) => {
    if (issue.input === undefined) return t("field.required")
    return t("field.invalid_string")
  },
}).min(1, { error: t("field.too_short") })
```

Or use global config for app-wide localization:

```typescript
z.config({
  customError: (issue) => {
    const t = getTranslation(currentLocale)
    switch (issue.code) {
      case "invalid_type": return t("validation.invalid_type")
      case "too_small": return t("validation.too_small", { min: issue.minimum })
      default: return t("validation.invalid")
    }
  },
})
```

```

### references/advanced-features.md

```markdown
# Advanced Features Reference

## Codecs

Bidirectional transforms — decode (parse) and encode (serialize).

```typescript
const DateCodec = z.codec(z.iso.datetime(), z.date(), {
  decode: (s) => new Date(s),           // string → Date
  encode: (d) => d.toISOString(),       // Date → string
})

const parsed = DateCodec.parse("2024-01-01T00:00:00Z") // Date
const serialized = DateCodec.encode(parsed)              // "2024-01-01T00:00:00.000Z"
```

### Built-in Codecs

```typescript
// ISO datetime codec (string ↔ Date)
z.iso.datetime()

// Use with codec for custom decode/encode
z.codec(z.iso.datetime(), z.date(), {
  decode: (s) => new Date(s),
  encode: (d) => d.toISOString(),
})
```

### When to Use Codecs vs Transforms

| | `.transform()` | `z.codec()` |
|---|---|---|
| Direction | One-way (input → output) | Bidirectional |
| Use when | Only parsing | Round-trip (parse + serialize) |
| Encoding | Not supported | `schema.encode(value)` |

## Branded Types

Nominal typing — prevents mixing structurally identical types.

```typescript
const USD = z.number().brand<"USD">()
const EUR = z.number().brand<"EUR">()

type USD = z.infer<typeof USD> // number & { __brand: "USD" }
type EUR = z.infer<typeof EUR> // number & { __brand: "EUR" }

// TypeScript prevents mixing
function pay(amount: USD) { /* ... */ }
const euros = EUR.parse(100)
pay(euros) // TypeScript error!
```

### Common Use Cases

```typescript
// Prevent ID mixing
const UserId = z.string().brand<"UserId">()
const PostId = z.string().brand<"PostId">()

// Type-safe units
const Meters = z.number().brand<"Meters">()
const Feet = z.number().brand<"Feet">()

// Validated strings
const Email = z.email().brand<"Email">()
const Slug = z.string().regex(/^[a-z0-9-]+$/).brand<"Slug">()
```

## .readonly()

Output type becomes `Readonly<T>`.

```typescript
const Config = z.object({
  host: z.string(),
  port: z.number(),
}).readonly()

type Config = z.infer<typeof Config>
// Readonly<{ host: string; port: number }>
```

## Metadata and Registries

### .meta()

Attach arbitrary metadata to schemas.

```typescript
const UserSchema = z.object({
  name: z.string().meta({ label: "Full Name", placeholder: "Enter name" }),
  email: z.email().meta({ label: "Email Address" }),
})
```

### Registries

```typescript
// Global registry
z.globalRegistry.register(UserSchema, {
  id: "User",
  description: "User account schema",
})

// Custom typed registry
const uiRegistry = z.registry<{ label: string; widget: string }>()
uiRegistry.register(UserSchema.shape.name, {
  label: "Name",
  widget: "text-input",
})
```

## JSON Schema

### z.toJSONSchema(schema)

Convert Zod schema to JSON Schema.

```typescript
const jsonSchema = z.toJSONSchema(UserSchema)
// {
//   type: "object",
//   properties: {
//     name: { type: "string" },
//     email: { type: "string", format: "email" },
//   },
//   required: ["name", "email"]
// }
```

### z.fromJSONSchema(jsonSchema)

Convert JSON Schema to Zod schema.

```typescript
const zodSchema = z.fromJSONSchema({
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "integer", minimum: 0 },
  },
  required: ["name"],
})
```

## z.function()

Validate function arguments and return type.

```typescript
const MyFunc = z.function(
  z.tuple([z.string(), z.number()]), // args
  z.boolean()                         // return type
)

type MyFunc = z.infer<typeof MyFunc>
// (arg0: string, arg1: number) => boolean
```

## z.instanceof()

Check if value is an instance of a class.

```typescript
const ErrorSchema = z.instanceof(Error)
ErrorSchema.parse(new Error("test")) // passes
ErrorSchema.parse("not an error")    // fails
```

## Template Literals

```typescript
const UserId = z.templateLiteral([z.literal("user_"), z.string()])
// Matches: "user_abc", "user_123"
// Rejects: "abc", "admin_123"

const Route = z.templateLiteral([
  z.literal("/api/"),
  z.enum(["users", "posts"]),
  z.literal("/"),
  z.string(),
])
// Matches: "/api/users/123", "/api/posts/abc"
```

## Standard Schema

Zod schemas implement the Standard Schema interface, making them compatible with any library that supports it.

```typescript
import type { StandardSchema } from "@standard-schema/spec"

function validate(schema: StandardSchema, data: unknown) {
  return schema["~standard"].validate(data)
}

// Works with Zod schemas
validate(UserSchema, data)
```

```

### references/anti-patterns.md

```markdown
# Zod Anti-Patterns

## Table of Contents

- Using parse() with try/catch instead of safeParse()
- Sync parse with async refinements
- Manual type definitions alongside schemas
- Deprecated string format chaining (v4)
- Using z.nativeEnum() (v4)
- Using required_error/invalid_type_error (v4)
- Using z.formatError() (v4)
- Throwing inside refinements or transforms
- z.coerce.boolean() for string booleans
- Assuming z.object() preserves unknown keys
- z.union() for tagged objects
- reportInput in production
- z.lazy() for recursive schemas (v4)
- Duplicating field definitions

## Using parse() with try/catch instead of safeParse()

```typescript
// BAD: verbose, catches unrelated errors
try {
  const user = UserSchema.parse(data)
  return { success: true, data: user }
} catch (e) {
  if (e instanceof z.ZodError) {
    return { success: false, errors: e.issues }
  }
  throw e
}

// GOOD: discriminated result
const result = UserSchema.safeParse(data)
if (result.success) {
  return { success: true, data: result.data }
} else {
  return { success: false, errors: result.error.issues }
}
```

## Sync parse with async refinements

```typescript
// BAD: throws because refinement is async
const Schema = z.email().refine(async (e) => !(await db.exists(e)))
Schema.safeParse(data) // throws

// GOOD: use safeParseAsync
await Schema.safeParseAsync(data)
```

## Manual type definitions alongside schemas

```typescript
// BAD: types drift from schema
interface User {
  name: string
  email: string
}

const UserSchema = z.object({
  name: z.string(),
  email: z.email(),
  age: z.number(), // added to schema, forgot interface
})

// GOOD: infer from schema
type User = z.infer<typeof UserSchema>
```

## Deprecated string format chaining (v4)

```typescript
// BAD: deprecated in v4
z.string().email()
z.string().url()
z.string().uuid()

// GOOD: top-level format functions
z.email()
z.url()
z.uuid()
```

## Using z.nativeEnum() (v4)

```typescript
enum Status { Active = "active", Inactive = "inactive" }

// BAD: removed in v4
z.nativeEnum(Status)

// GOOD: unified z.enum()
z.enum(Status)
```

## Using required_error/invalid_type_error (v4)

```typescript
// BAD: removed in v4
z.string({ required_error: "Required", invalid_type_error: "Not a string" })
z.number().min(5, { message: "Too small" })

// GOOD: unified error parameter
z.string({ error: "Required" })
z.number().min(5, { error: "Too small" })
z.number().min(5, "Too small") // shorthand
```

## Using z.formatError() (v4)

```typescript
// BAD: deprecated
z.formatError(error)

// GOOD: use the right formatter
z.flattenError(error)   // for flat forms
z.treeifyError(error)   // for nested structures
z.prettifyError(error)  // for logging
```

## Throwing inside refinements or transforms

```typescript
// BAD: bypasses Zod error handling
z.number().refine((n) => {
  if (n <= 0) throw new Error("Must be positive")
  return true
})

z.string().transform((val) => {
  const n = parseInt(val)
  if (isNaN(n)) throw new Error("Not a number")
  return n
})

// GOOD: return boolean from refine
z.number().refine((n) => n > 0, { error: "Must be positive" })

// GOOD: validate then transform
z.string()
  .refine((val) => !isNaN(parseInt(val)), { error: "Not numeric" })
  .transform((val) => parseInt(val))

// BEST: pipe for staged parsing
z.string().pipe(z.coerce.number()).pipe(z.number().positive())
```

## z.coerce.boolean() for string booleans

```typescript
// BAD: Boolean("false") === true
z.coerce.boolean().parse("false") // true
z.coerce.boolean().parse("0")     // true

// GOOD: z.stringbool() handles string booleans correctly
z.stringbool().parse("false") // false
z.stringbool().parse("0")     // false
```

## Assuming z.object() preserves unknown keys

```typescript
// BAD: unknown keys are silently stripped
const data = { name: "Alice", role: "admin", debug: true }
z.object({ name: z.string() }).parse(data)
// { name: "Alice" } — role and debug are gone

// GOOD: choose explicitly
z.strictObject({ name: z.string() }) // rejects unknown
z.looseObject({ name: z.string() })  // preserves unknown
```

## z.union() for tagged objects

```typescript
// BAD: sequential matching, poor error messages
z.union([
  z.object({ type: z.literal("a"), value: z.string() }),
  z.object({ type: z.literal("b"), count: z.number() }),
])

// GOOD: O(1) dispatch on discriminator
z.discriminatedUnion("type", [
  z.object({ type: z.literal("a"), value: z.string() }),
  z.object({ type: z.literal("b"), count: z.number() }),
])
```

## reportInput in production

```typescript
// BAD: leaks sensitive data into error logs
app.post("/login", (req, res) => {
  const result = schema.safeParse(req.body, { reportInput: true })
  if (!result.success) {
    logger.error(result.error.issues) // may contain passwords
  }
})

// GOOD: development only
schema.safeParse(req.body, {
  reportInput: process.env.NODE_ENV === "development",
})
```

## z.lazy() for recursive schemas (v4)

```typescript
// BAD: z.lazy() removed in v4
const Tree = z.object({
  value: z.string(),
  children: z.lazy(() => z.array(Tree)),
})

// GOOD: getter pattern
const Tree = z.object({
  value: z.string(),
  get children() {
    return z.array(Tree).optional()
  },
})
```

## Duplicating field definitions

```typescript
// BAD: fields duplicated — will drift
const CreateUser = z.object({ name: z.string(), email: z.email() })
const UpdateUser = z.object({ name: z.string().optional(), email: z.email().optional() })

// GOOD: derive from base
const User = z.object({ name: z.string(), email: z.email() })
const CreateUser = User
const UpdateUser = User.partial()
```

```

### references/boundary-architecture.md

```markdown
# Boundary Architecture — Where Zod Fits

## Overview

Zod belongs at **system boundaries** — the points where your application receives data it doesn't control. Parse once at the boundary, then pass typed data inward. Domain logic should never see `unknown`.

## Express / Fastify — Route Handler vs Middleware

### In the Route Handler (Recommended for Most Cases)

```typescript
app.post("/api/users", (req, res) => {
  const result = CreateUserSchema.safeParse(req.body)
  if (!result.success) {
    return res.status(400).json({
      errors: z.flattenError(result.error).fieldErrors,
    })
  }
  const user = await createUser(result.data)
  res.status(201).json(user)
})
```

### As Middleware (For Shared Validation Logic)

```typescript
function validate<T extends z.ZodType>(schema: T) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body)
    if (!result.success) {
      return res.status(400).json({
        errors: z.flattenError(result.error).fieldErrors,
      })
    }
    req.body = result.data // typed from here on
    next()
  }
}

app.post("/api/users", validate(CreateUserSchema), (req, res) => {
  // req.body is already validated and typed
  const user = await createUser(req.body)
  res.status(201).json(user)
})
```

### When to Use Each

| Approach | Use When |
|----------|----------|
| Route handler | Schema is specific to one route, custom error responses needed |
| Middleware | Same schema/error format across many routes |

## tRPC — `.input()` Parsing

tRPC handles boundary parsing for you via `.input()`:

```typescript
export const userRouter = router({
  create: publicProcedure
    .input(CreateUserSchema) // tRPC calls safeParse internally
    .mutation(async ({ input }) => {
      // input is fully typed — z.infer<typeof CreateUserSchema>
      return createUser(input)
    }),

  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return getUser(input.id)
    }),
})
```

You don't call `safeParse()` yourself — tRPC does it and returns a typed error response automatically.

## Next.js Server Actions

```typescript
"use server"

const CreatePostSchema = z.object({
  title: z.string().min(1),
  body: z.string().min(10),
})

export async function createPost(formData: FormData) {
  // Parse at the top of the action
  const result = CreatePostSchema.safeParse({
    title: formData.get("title"),
    body: formData.get("body"),
  })

  if (!result.success) {
    return { errors: z.flattenError(result.error).fieldErrors }
  }

  // Typed from here on
  await db.posts.create({ data: result.data })
  revalidatePath("/posts")
}
```

### Next.js Route Handlers

```typescript
export async function POST(request: Request) {
  const body = await request.json()
  const result = CreateUserSchema.safeParse(body)

  if (!result.success) {
    return Response.json(
      { errors: z.flattenError(result.error).fieldErrors },
      { status: 400 }
    )
  }

  const user = await createUser(result.data)
  return Response.json(user, { status: 201 })
}
```

## React Hook Form — zodResolver

The form library handles the boundary:

```typescript
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"

const ProfileSchema = z.object({
  name: z.string().min(1),
  bio: z.string().max(500).optional(),
})

type ProfileForm = z.infer<typeof ProfileSchema>

function ProfileEditor() {
  const { register, handleSubmit, formState: { errors } } = useForm<ProfileForm>({
    resolver: zodResolver(ProfileSchema),
  })

  const onSubmit = (data: ProfileForm) => {
    // data is already validated — no safeParse needed
    updateProfile(data)
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      {errors.name && <span>{errors.name.message}</span>}
    </form>
  )
}
```

## Environment Variables — Startup Parsing

Parse env vars once at application startup. Fail fast if the environment is misconfigured.

### Manual Approach

```typescript
// config/env.ts — parsed at import time
const EnvSchema = z.object({
  DATABASE_URL: z.url(),
  API_KEY: z.string().min(1),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
})

export const env = EnvSchema.parse(process.env)
// App crashes at startup if env is invalid — this is intentional
```

### With t3-env

```typescript
import { createEnv } from "@t3-oss/env-nextjs"

export const env = createEnv({
  server: {
    DATABASE_URL: z.url(),
    API_KEY: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.url(),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    API_KEY: process.env.API_KEY,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
  },
})
```

## External API Responses

Always validate data coming from external services — their schemas can change without warning.

```typescript
const WeatherResponse = z.object({
  temperature: z.number(),
  humidity: z.number().min(0).max(100),
  conditions: z.string(),
})

async function getWeather(city: string) {
  const res = await fetch(`https://api.weather.example/v1/${city}`)
  const json = await res.json()

  // Parse at the boundary — don't trust external data
  const result = WeatherResponse.safeParse(json)
  if (!result.success) {
    logger.warn("weather_api_schema_mismatch", {
      schema: "WeatherResponse",
      fieldErrors: z.flattenError(result.error).fieldErrors,
    })
    throw new ExternalServiceError("Weather API returned unexpected shape")
  }

  return result.data // typed Weather
}
```

## Database Layer

Parse DB results when the schema might drift from the actual DB shape (e.g., after migrations, with untyped ORMs, or with raw SQL).

```typescript
const UserRow = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  created_at: z.coerce.date(),
})

async function getUserById(id: string) {
  const row = await db.query("SELECT * FROM users WHERE id = $1", [id])
  // Validate if using raw SQL or untyped ORM
  return UserRow.parse(row)
}
```

If you use a fully typed ORM like Prisma or Drizzle that generates types from your schema, additional Zod parsing of DB results is usually unnecessary.

## Summary: Boundary Layer Checklist

| Boundary | Who Parses | Schema Location |
|----------|------------|-----------------|
| Express/Fastify route | Your middleware or handler | `api/[resource]/schemas.ts` |
| tRPC procedure | tRPC via `.input()` | Inline or co-located |
| Next.js Server Action | Top of action function | Co-located with action |
| React Hook Form | zodResolver | `features/[name]/form-schema.ts` |
| Env vars | Startup (parse, not safeParse) | `config/env.ts` |
| External API response | After fetch, before use | Co-located with API client |
| Database results | After query (if untyped) | Co-located with data access |
| Message queue | Top of consumer handler | Co-located with consumer |

```

### references/linter-and-ci.md

```markdown
# Linting and CI Rules for Zod

## Overview

Mechanical enforcement catches mistakes before code review. Use ESLint rules, CI checks, and static analysis tools to enforce Zod best practices automatically.

## ESLint: `no-restricted-syntax` Rules

### Ban `parse()` in Application Code

Force `safeParse()` usage. `parse()` throws, which leads to unhandled exceptions.

```jsonc
// .eslintrc.json
{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "CallExpression[callee.property.name='parse'][callee.object.type!='ThisExpression']",
        "message": "Use safeParse() instead of parse(). parse() throws on invalid input. See: rules/parse-use-safeParse.md"
      }
    ]
  }
}
```

To allow `parse()` in specific files (e.g., env config where crashing on invalid env is intentional), use ESLint overrides:

```jsonc
{
  "overrides": [
    {
      "files": ["**/config/env.ts", "**/env.ts"],
      "rules": {
        "no-restricted-syntax": "off"
      }
    }
  ]
}
```

### Detect `reportInput` Usage

`reportInput: true` leaks sensitive data in production.

```jsonc
{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "Property[key.name='reportInput'][value.value=true]",
        "message": "reportInput: true leaks sensitive data in production. Use: reportInput: process.env.NODE_ENV === 'development'. See: rules/error-input-security.md"
      }
    ]
  }
}
```

### Ban `z.nativeEnum()` (Removed in v4)

```jsonc
{
  "selector": "CallExpression[callee.property.name='nativeEnum']",
  "message": "z.nativeEnum() is removed in Zod v4. Use z.enum(YourTSEnum) instead. See: rules/migrate-native-enum.md"
}
```

### Ban `z.string().email()` (Use `z.email()`)

```jsonc
{
  "selector": "CallExpression[callee.property.name='email'][callee.object.callee.property.name='string']",
  "message": "Use z.email() instead of z.string().email() in Zod v4. See: rules/migrate-string-formats.md"
}
```

### Full ESLint Config Example

```jsonc
{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "CallExpression[callee.property.name='parse'][callee.object.type!='ThisExpression']",
        "message": "Use safeParse() instead of parse(). See: rules/parse-use-safeParse.md"
      },
      {
        "selector": "Property[key.name='reportInput'][value.value=true]",
        "message": "reportInput: true leaks sensitive data. See: rules/error-input-security.md"
      },
      {
        "selector": "CallExpression[callee.property.name='nativeEnum']",
        "message": "z.nativeEnum() removed in v4. Use z.enum(). See: rules/migrate-native-enum.md"
      }
    ]
  },
  "overrides": [
    {
      "files": ["**/config/env.ts", "**/env.ts"],
      "rules": {
        "no-restricted-syntax": "off"
      }
    }
  ]
}
```

## CI: Schema Snapshot Regression

Detect unintended schema changes by committing JSON Schema snapshots and failing CI when they drift.

### Setup

```typescript
// scripts/export-schemas.ts
import { z } from "zod"
import { writeFileSync, mkdirSync } from "fs"
import { UserSchema, OrderSchema } from "../src/api/schemas"

const schemas = {
  User: UserSchema,
  Order: OrderSchema,
}

mkdirSync("snapshots", { recursive: true })

for (const [name, schema] of Object.entries(schemas)) {
  const jsonSchema = z.toJSONSchema(schema)
  writeFileSync(
    `snapshots/${name}.json`,
    JSON.stringify(jsonSchema, null, 2) + "\n"
  )
}
```

### CI Check

```yaml
# .github/workflows/schema-check.yml
name: Schema Snapshot Check
on: [pull_request]

jobs:
  schema-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
      - run: npm ci
      - run: npx tsx scripts/export-schemas.ts
      - name: Check for schema drift
        run: |
          if git diff --exit-code snapshots/; then
            echo "Schemas unchanged"
          else
            echo "::error::Schema snapshots have changed. Review the diff and update snapshots if intentional."
            git diff snapshots/
            exit 1
          fi
```

### Workflow

1. Developer changes a schema
2. CI runs `export-schemas.ts` and diffs against committed snapshots
3. If diff exists, CI fails with the exact changes shown
4. Developer reviews the diff, runs `npx tsx scripts/export-schemas.ts` locally, commits updated snapshots

## Unused Schema Detection

Find schemas that are defined but never imported.

### With knip

```bash
npx knip --include exports
```

knip detects unused exports including schemas. Configure in `knip.json`:

```jsonc
{
  "entry": ["src/index.ts"],
  "project": ["src/**/*.ts"],
  "ignore": ["**/*.test.ts", "**/*.spec.ts"]
}
```

### With ts-prune

```bash
npx ts-prune | grep -i schema
```

Shows exported symbols with no imports. Review and remove dead schemas.

## Circular Dependency Detection

Schemas that import each other create circular dependencies that cause runtime crashes.

### With madge

```bash
# Find circular dependencies
npx madge --circular --extensions ts src/

# Generate dependency graph
npx madge --image graph.svg --extensions ts src/
```

### CI Integration

```yaml
- name: Check circular dependencies
  run: |
    npx madge --circular --extensions ts src/
    if [ $? -ne 0 ]; then
      echo "::error::Circular dependencies detected"
      exit 1
    fi
```

## Custom ESLint Rule Messages

Include remediation instructions in ESLint messages so developers know exactly what to do:

```jsonc
{
  "selector": "CallExpression[callee.property.name='parse']",
  "message": "Use safeParse() instead of parse().\n\nparse() throws ZodError on invalid input.\nsafeParse() returns { success, data | error }.\n\nReplace:\n  schema.parse(data)\nWith:\n  const result = schema.safeParse(data)\n  if (!result.success) { /* handle error */ }\n\nSee: rules/parse-use-safeParse.md"
}
```

## Summary

| Tool | What It Catches | When |
|------|----------------|------|
| ESLint `no-restricted-syntax` | Banned API usage (parse, reportInput, nativeEnum) | On save / pre-commit |
| Schema snapshot CI | Unintended schema changes | On pull request |
| knip / ts-prune | Unused schemas | On pull request / periodic |
| madge | Circular schema dependencies | On pull request |

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# Zod Skill

Created by **[Anivar Aravind](https://anivar.net)**

An AI agent skill for writing, validating, and debugging Zod v4 schemas with modern best practices.

## The Problem

AI agents often generate outdated Zod v3 patterns — `z.string().email()` instead of `z.email()`, `z.nativeEnum()` instead of `z.enum()`, `required_error` instead of the `error` parameter — and miss critical parsing pitfalls like using `parse()` instead of `safeParse()`, forgetting `parseAsync` for async refinements, or assuming `z.object()` preserves unknown keys. These produce schemas that compile but silently misbehave at runtime.

## This Solution

27 rules with incorrect→correct code examples that teach agents Zod v4's actual API behavior, schema design patterns, error handling, architectural placement, observability, and TypeScript integration. Each rule targets a specific mistake and shows exactly how to fix it.

## Install

```bash
npx skills add anivar/zod-skill -g
```

Or with full URL:

```bash
npx skills add https://github.com/anivar/zod-skill
```

## Baseline

- zod ^4.0.0
- TypeScript ^5.5

## What's Inside

### 27 Rules Across 9 Categories

| Priority | Category | Rules | Impact |
|----------|----------|-------|--------|
| 1 | Parsing & Type Safety | 3 | CRITICAL |
| 2 | Schema Design | 4 | CRITICAL |
| 3 | Refinements & Transforms | 3 | HIGH |
| 4 | Error Handling | 3 | HIGH |
| 5 | Performance & Composition | 3 | MEDIUM |
| 6 | v4 Migration | 3 | MEDIUM |
| 7 | Advanced Patterns | 3 | MEDIUM |
| 8 | Architecture & Boundaries | 3 | CRITICAL/HIGH |
| 9 | Observability | 2 | HIGH/MEDIUM |

Each rule file contains:
- Why it matters
- Incorrect code with explanation
- Correct code with explanation
- Decision tables and additional context

### 9 Deep-Dive References

| Reference | Covers |
|-----------|--------|
| `schema-types.md` | All primitives, string formats, numbers, enums, dates, files, JSON |
| `parsing-and-inference.md` | parse, safeParse, parseAsync, z.infer, z.input, coercion |
| `objects-and-composition.md` | object/strict/loose, pick, omit, partial, recursive, unions, tuples |
| `refinements-and-transforms.md` | refine, superRefine, transform, pipe, defaults, catch |
| `error-handling.md` | ZodError, flattenError, treeifyError, error customization, i18n |
| `advanced-features.md` | Codecs, branded types, JSON Schema, registries, Standard Schema |
| `anti-patterns.md` | 14 common mistakes with BAD/GOOD examples |
| `boundary-architecture.md` | Where Zod fits: Express, tRPC, Next.js, React Hook Form, env, external APIs |
| `linter-and-ci.md` | ESLint rules, CI schema snapshots, unused schema detection, circular deps |

## Structure

```
├── SKILL.md                          # Entry point for AI agents
├── AGENTS.md                         # Compiled guide with all rules expanded
├── rules/                            # Individual rules (Incorrect→Correct)
│   ├── parse-*                       # Parsing & type safety (CRITICAL)
│   ├── schema-*                      # Schema design (CRITICAL)
│   ├── refine-*                      # Refinements & transforms (HIGH)
│   ├── error-*                       # Error handling (HIGH)
│   ├── perf-*                        # Performance & composition (MEDIUM)
│   ├── migrate-*                     # v4 migration (MEDIUM)
│   ├── pattern-*                     # Advanced patterns (MEDIUM)
│   ├── arch-*                        # Architecture & boundaries (CRITICAL/HIGH)
│   └── observe-*                     # Observability (HIGH/MEDIUM)
└── references/                       # Deep-dive reference docs
    ├── schema-types.md
    ├── parsing-and-inference.md
    ├── objects-and-composition.md
    ├── refinements-and-transforms.md
    ├── error-handling.md
    ├── advanced-features.md
    ├── anti-patterns.md
    ├── boundary-architecture.md
    └── linter-and-ci.md
```

## Ecosystem — Skills by [Anivar Aravind](https://anivar.net)

### Testing Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [jest-skill](https://github.com/anivar/jest-skill) | Jest best practices — mock design, async testing, matchers, timers, snapshots | `npx skills add anivar/jest-skill -g` |
| [zod-testing](https://github.com/anivar/zod-testing) | Zod schema testing — safeParse, mock data, property-based | `npx skills add anivar/zod-testing -g` |
| [msw-skill](https://github.com/anivar/msw-skill) | MSW 2.0 API mocking — handlers, responses, GraphQL | `npx skills add anivar/msw-skill -g` |
| [redux-saga-testing](https://github.com/anivar/redux-saga-testing) | Redux-Saga testing — expectSaga, testSaga, providers | `npx skills add anivar/redux-saga-testing -g` |

### Library & Framework Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [redux-saga-skill](https://github.com/anivar/redux-saga-skill) | Redux-Saga effects, fork model, channels, RTK | `npx skills add anivar/redux-saga-skill -g` |
| [msw-skill](https://github.com/anivar/msw-skill) | MSW 2.0 handlers, responses, migration | `npx skills add anivar/msw-skill -g` |

### Engineering Analysis
| Skill | What it covers | Install |
|-------|---------------|---------|
| [contributor-codebase-analyzer](https://github.com/anivar/contributor-codebase-analyzer) | Code analysis, annual reviews, promotion readiness | `npx skills add anivar/contributor-codebase-analyzer -g` |

## Author

**[Anivar Aravind](https://anivar.net)** — Building AI agent skills for modern JavaScript/TypeScript development.

## License

MIT

```

### _meta.json

```json
{
  "owner": "anivar",
  "slug": "zod-skill",
  "displayName": "Zod",
  "latest": {
    "version": "1.0.0",
    "publishedAt": 1772687950970,
    "commit": "https://github.com/openclaw/skills/commit/9e25119caa590785d63fdf5ce23118054f6a5a82"
  },
  "history": []
}

```

zod | SkillHub