Zod v4 Validation

Guidance for using Zod v4 correctly, including import conventions, schema patterns, and type inference in TypeScript projects.

CopilotCursorKilo
AuthorNeexoCore
Apply to**/*.{ts,tsx}
Updated
zodvalidationtypescript

Overview

Neexo SaaS playbooks default to Zod v4. A consuming repository may pin an older major (for example Zod 3) via its own project rules — those win on conflict. Do not mix Zod v3 and v4 APIs in the same project.

Import Convention

On Zod v4, import { z } from "zod" resolves the v4 API. Neexo convention still prefers the explicit subpath for clarity and for repos that previously mixed majors:

// Preferred Neexo convention (Zod v4)
import { z } from "zod/v4";

// Also valid on Zod v4 — same major API via the package root
import { z } from "zod";

Do not assume import { z } from "zod" always means Zod v3. Check the installed zod major in package.json / the lockfile first.

If the project is still on Zod 3, use import { z } from "zod" and follow that repo's rules — do not force "zod/v4".

Schema Patterns

import { z } from "zod/v4";

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1).max(100),
  role: z.enum(["admin", "editor", "viewer"]),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;

// Record requires two arguments in Zod v4
const MetadataSchema = z.record(z.string(), z.unknown());

Validator Organization

  • One validator file per domain: src/lib/validators/users.ts, src/lib/validators/orders.ts
  • Export named schemas and their inferred types together
  • Keep schemas close to where they are used (actions, API routes, forms)

Server Action Pattern

import { z } from "zod/v4";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export async function createUser(input: unknown) {
  const parsed = CreateUserSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: parsed.error.message };
  }
  // Use parsed.data — fully typed
}

Common Pitfalls

  • z.record() requires two arguments in Zod v4: z.record(z.string(), z.unknown()) — one argument throws
  • z.coerce.date() parses strings to dates — useful for form inputs
  • Do not mix Zod v3 and v4 imports in the same project
  • Prefer "zod/v4" in Neexo Zod v4 apps for explicitness; do not rewrite working Zod 3 repos without a planned migration

Raw content

Copy into your project — e.g. .instructions.md, .agent.md, or SKILL.md

## Overview

Neexo SaaS playbooks default to Zod v4. A consuming repository may pin an older major (for example Zod 3) via its own project rules — those win on conflict. Do not mix Zod v3 and v4 APIs in the same project.

## Import Convention

On Zod v4, `import { z } from "zod"` resolves the v4 API. Neexo convention still prefers the explicit subpath for clarity and for repos that previously mixed majors:

```tsx
// Preferred Neexo convention (Zod v4)
import { z } from "zod/v4";

// Also valid on Zod v4 — same major API via the package root
import { z } from "zod";
```

Do **not** assume `import { z } from "zod"` always means Zod v3. Check the installed `zod` major in `package.json` / the lockfile first.

If the project is still on Zod 3, use `import { z } from "zod"` and follow that repo's rules — do not force `"zod/v4"`.

## Schema Patterns

```tsx
import { z } from "zod/v4";

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  name: z.string().min(1).max(100),
  role: z.enum(["admin", "editor", "viewer"]),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;

// Record requires two arguments in Zod v4
const MetadataSchema = z.record(z.string(), z.unknown());
```

## Validator Organization

- One validator file per domain: `src/lib/validators/users.ts`, `src/lib/validators/orders.ts`
- Export named schemas and their inferred types together
- Keep schemas close to where they are used (actions, API routes, forms)

## Server Action Pattern

```tsx
import { z } from "zod/v4";

const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1),
});

export async function createUser(input: unknown) {
  const parsed = CreateUserSchema.safeParse(input);
  if (!parsed.success) {
    return { success: false, error: parsed.error.message };
  }
  // Use parsed.data — fully typed
}
```

## Common Pitfalls

- `z.record()` requires two arguments in Zod v4: `z.record(z.string(), z.unknown())` — one argument throws
- `z.coerce.date()` parses strings to dates — useful for form inputs
- Do not mix Zod v3 and v4 imports in the same project
- Prefer `"zod/v4"` in Neexo Zod v4 apps for explicitness; do not rewrite working Zod 3 repos without a planned migration

Next steps

Matched by shared tags and category. Browse all instructions