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