Skip to content

Commit bb393ac

Browse files
committed
feat(zod): add use-zod skill plugin
Hand-written workflow skill for Zod schema validation, mirroring the mastra plugin pattern from #161. Single trigger-heavy SKILL.md plus three references (versions, schemas, parsing-and-errors) covering both Zod v3 and v4 with inline migration callouts. Key v3 → v4 differences surfaced: - err.format() / err.flatten() → z.treeifyError() / z.flattenError() - separate { message, errorMap } → unified { error } param - z.lazy + ZodType<T> annotation → property getters for recursion - .superRefine() (deprecated) → .check() - new in v4: z.codec, z.prettifyError, .overwrite, zod/mini No MCP server (zod has no official docs MCP server). Skills-only plugin with skills: ['./skills/'].
1 parent cb803b4 commit bb393ac

5 files changed

Lines changed: 872 additions & 0 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "zod",
3+
"version": "1.0.0",
4+
"description": "TypeScript-first schema validation with static type inference - version-aware skill for Zod v3 and v4",
5+
"author": {
6+
"name": "Colin McDonnell",
7+
"url": "https://github.com/colinhacks"
8+
},
9+
"homepage": "https://zod.dev",
10+
"repository": "https://github.com/colinhacks/zod",
11+
"license": "MIT",
12+
"keywords": [
13+
"zod",
14+
"validation",
15+
"schema",
16+
"typescript"
17+
],
18+
"skills": [
19+
"./skills/"
20+
]
21+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
---
2+
name: use-zod
3+
description: 'Answer questions about the Zod schema validation library and help build schemas, parsers, refinements, transforms, codecs, and error formatters. Use when developers: (1) ask about Zod APIs like `z.object`, `z.string`, `z.array`, `z.union`, `z.discriminatedUnion`, `parse`, `safeParse`, `z.infer`; (2) define request/response/form schemas in TypeScript; (3) handle `ZodError` or customize error messages; (4) migrate between Zod v3 and v4 (entry-point split, `formatError` → `treeifyError`/`prettifyError`, unified `error` param replacing `message`/`errorMap`). Triggers on: "zod", "z.object", "z.string", "z.array", "z.union", "z.infer", "z.input", "z.output", "ZodError", "$ZodError", "safeParse", "parseAsync", "z.codec", "treeifyError", "prettifyError", "flattenError", "discriminatedUnion", "zod/v4", "zod/v3", "zod/mini", "z.coerce", "superRefine".'
4+
---
5+
6+
## Prerequisites
7+
8+
Before writing Zod code, verify the installed version and entry points in the current project:
9+
10+
```bash
11+
# installed version (drives everything below)
12+
cat node_modules/zod/package.json 2>/dev/null | jq -r .version
13+
14+
# subpath exports — confirms which import paths resolve
15+
cat node_modules/zod/package.json 2>/dev/null | jq '.exports | keys'
16+
```
17+
18+
If `zod` is missing, install only what the task requires:
19+
20+
```bash
21+
# v4 (current default since zod@4.0.0)
22+
pnpm add zod # or: bun add zod / npm i zod / yarn add zod
23+
24+
# pin to v3 only when the project explicitly requires it
25+
pnpm add zod@^3
26+
```
27+
28+
Detect the package manager from the lockfile (`pnpm-lock.yaml` → pnpm, `bun.lockb` → bun, `package-lock.json` → npm, `yarn.lock` → yarn).
29+
30+
## Critical: Do Not Trust Internal Knowledge
31+
32+
Zod 4 (released 2026) was a major rewrite. Many APIs that were canonical in v3 are now deprecated, renamed, or removed. Examples that are commonly miswritten from training data:
33+
34+
- `err.format()` / `err.flatten()` (v3 instance methods) — in v4 these are top-level functions: `z.treeifyError(err)` / `z.flattenError(err)`. `z.formatError()` exists but is **deprecated** in favour of `z.treeifyError()`.
35+
- `z.string({ message, errorMap })` (v3) — v4 unifies these into a single `error` param: `z.string({ error: "Bad!" })` or `z.string({ error: (iss) => "..." })`.
36+
- `.superRefine()` — deprecated in v4. Use `.check()` instead.
37+
- `error instanceof z.ZodError` — works for the regular `zod` package; for `zod/mini` use `error instanceof z.core.$ZodError` (the parent class).
38+
- Codecs (`z.codec(...)`) — only exist in `zod@4.1+`. Do not suggest them on v3 or earlier 4.x.
39+
40+
When working with Zod:
41+
42+
1. Resolve the installed version first (`node_modules/zod/package.json`).
43+
2. If unsure whether an API exists in the installed version, grep the bundled `.d.ts`: `rg -n "treeifyError|formatError" node_modules/zod/dist`.
44+
3. Check upstream docs **at the matching version pin** (links below in [`references/versions.md`](references/versions.md)) — not at `main`, which tracks the latest release.
45+
4. Run typecheck after every change. Zod schemas are heavily inferred and silent type drift is rare.
46+
5. Never invent method names. If the user references an API you don't recognise, look it up before answering.
47+
6. Surface deprecations to the user instead of silently emitting either pattern.
48+
49+
If documentation cannot be found locally or remotely to back an answer, say so explicitly.
50+
51+
## Version detection — branch v4 vs v3 paths
52+
53+
```bash
54+
node -e "const v=require('zod/package.json').version; console.log(v.startsWith('4.')?'v4':v.startsWith('3.')?'v3':v)"
55+
```
56+
57+
| Detected | Default import | Errors API | Refinement API | Codecs |
58+
| --- | --- | --- | --- | --- |
59+
| v4 (≥4.0.0) | `import * as z from "zod"` | `z.treeifyError`, `z.prettifyError`, `z.flattenError` | `.refine()`, `.check()` | `z.codec()` (4.1+) |
60+
| v3 (≥3.0, <4.0) | `import { z } from "zod"` | `err.format()`, `err.flatten()` | `.refine()`, `.superRefine()` ||
61+
| v3.25.x bridge | `import * as z from "zod/v4"` opt-in to v4 alongside v3 | per the chosen path | per the chosen path ||
62+
63+
`zod@3.25` shipped both v3 (default) and v4 (under `zod/v4`) in the same package to ease migration. From `zod@4.0.0` onward, the root export is v4 and `zod/v3` is the back-compat path. See [`references/versions.md`](references/versions.md).
64+
65+
## Entry points (v4)
66+
67+
| Import | Use when |
68+
| --- | --- |
69+
| `import * as z from "zod"` | Default. Standard ergonomic API with chainable methods (`z.string().min(5)`). |
70+
| `import * as z from "zod/mini"` | Bundle-size-sensitive frontend code. Functional API: `z.string().check(z.minLength(5))`. ~64% smaller for trivial schemas. |
71+
| `import * as z from "zod/v3"` | Legacy code on v3 that you can't migrate yet, while consuming a `zod@4` package. |
72+
| `import * as z from "zod/v4-mini"` (within `zod@3.25`) | Forward-compat path for projects pinned to v3 that want to start adopting Mini. |
73+
74+
`zod/mini` and `zod` interop: schemas from one cannot be passed to the other's parse functions. Pick one per project unless you have a deliberate reason to mix.
75+
76+
## Authoring schemas
77+
78+
Concise cookbook of common patterns, each tagged with the version it applies to: [`references/schemas.md`](references/schemas.md).
79+
80+
Rules of thumb:
81+
82+
- **`z.object` is non-strict by default** — extra keys are stripped. Use `z.strictObject({...})` to reject extra keys, or `.passthrough()` (v3) / `.loose()` (v4) to preserve them.
83+
- **`.optional()` vs `.nullable()` vs `.nullish()`**`optional` allows `undefined`, `nullable` allows `null`, `nullish` allows both.
84+
- **Always export the type** with `z.infer<typeof Schema>`. Use `z.input` and `z.output` separately when the schema transforms (input ≠ output, e.g. `z.string().transform(s => s.length)`).
85+
- **Discriminated unions need a literal discriminator**`z.discriminatedUnion("type", [...])` is dramatically faster and produces better error messages than `z.union(...)` when shapes share a tag field.
86+
- **Recursive schemas** use different patterns per version: v4 uses object property **getters** (`get children() { return z.array(Self); }`); v3 uses `z.lazy(() => Schema)` plus an explicit `z.ZodType<Node>` annotation. See [`references/schemas.md`](references/schemas.md#recursive-schemas).
87+
88+
## Parsing & error handling
89+
90+
Concise reference: [`references/parsing-and-errors.md`](references/parsing-and-errors.md).
91+
92+
Quick map:
93+
94+
- `parse(input)` — throws on invalid; returns typed deep clone.
95+
- `safeParse(input)` — returns `{ success: true, data } | { success: false, error: ZodError }`.
96+
- `parseAsync` / `safeParseAsync` — required when the schema contains async refinements, transforms, or codecs.
97+
- `try { ... } catch (e) { if (e instanceof z.ZodError) e.issues }` — every error has an `.issues` array of `{ code, path, message, expected?, ... }`.
98+
99+
The `formatError``treeifyError` rename is the single most common source of broken v3-era examples. Surface it whenever rewriting v3 error-handling code.
100+
101+
## When typecheck or runtime fails
102+
103+
Before searching source code, check the most common Zod failure modes:
104+
105+
1. **`Invalid input: expected X`** with no `path` — top-level shape mismatch; verify the schema matches the expected outer type.
106+
2. **`Cannot read property 'parseAsync' of undefined`** — usually an import-path mismatch (`zod` vs `zod/mini`); methods on Mini schemas live on top-level functions instead.
107+
3. **`Type 'ZodError' is not assignable to type '$ZodError'`** — mixing `zod` and `zod/mini` schemas in the same code path.
108+
4. **Async refinement throws "Synchronous parsing not supported"** — switch the call site from `parse` to `parseAsync` (or `safeParse` to `safeParseAsync`).
109+
5. **`.superRefine` flagged as deprecated** (v4) — replace with `.check()` per [`references/parsing-and-errors.md`](references/parsing-and-errors.md).
110+
6. **Custom error not surfacing** — confirm you're using the unified `error` param (v4) and not the legacy `message`/`errorMap` shape (v3).
111+
112+
If the symptom is not listed:
113+
114+
```bash
115+
# resolve the error string inside installed source
116+
rg -n "error string fragment" node_modules/zod/dist
117+
```
118+
119+
## References
120+
121+
- [`references/versions.md`](references/versions.md) — entry points, version detection, v3 ↔ v4 API rename cheatsheet, links to upstream docs at version pins (v4.3.6, v3.25.76)
122+
- [`references/schemas.md`](references/schemas.md) — primitives, objects, arrays, unions, discriminated unions, recursion, refinements, transforms, codecs (v4.1+) — each example tagged `// v4`, `// v3`, or `// both`
123+
- [`references/parsing-and-errors.md`](references/parsing-and-errors.md)`parse` vs `safeParse` vs `parseAsync`, `ZodError` shape, `treeifyError`/`prettifyError`/`flattenError` (v4), `format`/`flatten` (v3), error customization
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Parsing & Error Handling
2+
3+
Purpose: choose the right parse method, read `ZodError` correctly, format errors for users, and customize messages — with v3 ↔ v4 differences flagged inline.
4+
5+
## Parse methods
6+
7+
```ts
8+
schema.parse(input); // throws ZodError on failure; returns typed deep clone
9+
schema.safeParse(input); // returns { success: true, data } | { success: false, error }
10+
schema.parseAsync(input); // required when schema has async refinements/transforms/codecs
11+
schema.safeParseAsync(input); // safe variant of the async path
12+
```
13+
14+
In v4, codec schemas also expose:
15+
16+
```ts
17+
schema.decode(input); // strongly-typed input; same runtime behavior as .parse
18+
schema.encode(value); // reverse direction (output → input)
19+
schema.safeDecode(input);
20+
schema.safeEncode(value);
21+
schema.decodeAsync(input);
22+
schema.encodeAsync(value);
23+
```
24+
25+
Pick `parse` when an exception path is acceptable (e.g. server controllers with framework-level error handlers). Pick `safeParse` for code that must branch on success without exceptions (e.g. form handlers, RPC).
26+
27+
### When to use the async variants
28+
29+
If **any** node in the schema graph is async, you must use `parseAsync` / `safeParseAsync`:
30+
31+
- `.refine(async (val) => ...)` — async refinement
32+
- `.transform(async (val) => ...)` — async transform
33+
- `z.codec(..., { decode: async (...) => ..., encode: async (...) => ... })` — async codec
34+
35+
Calling sync `.parse()` on a schema with async checks throws `Synchronous parsing not supported`. The fix is the call site, not the schema.
36+
37+
## Reading `ZodError`
38+
39+
```ts
40+
import * as z from "zod"; // v4
41+
42+
const schema = z.strictObject({
43+
username: z.string(),
44+
favoriteNumbers: z.array(z.number()),
45+
});
46+
47+
const result = schema.safeParse({
48+
username: 1234,
49+
favoriteNumbers: [1234, "4567"],
50+
extraKey: 1234,
51+
});
52+
53+
if (!result.success) {
54+
result.error.issues;
55+
// [
56+
// { expected: "string", code: "invalid_type", path: ["username"],
57+
// message: "Invalid input: expected string, received number" },
58+
// { expected: "number", code: "invalid_type", path: ["favoriteNumbers", 1],
59+
// message: "Invalid input: expected number, received string" },
60+
// { code: "unrecognized_keys", keys: ["extraKey"], path: [],
61+
// message: 'Unrecognized key: "extraKey"' },
62+
// ]
63+
}
64+
```
65+
66+
`result.error` is a `z.ZodError` (subclass of `z.core.$ZodError`). Each issue has at least `code`, `path`, and `message`; additional fields depend on the issue code (`expected`, `received`, `keys`, `minimum`, `maximum`, etc.).
67+
68+
> **`zod/mini` users**: parse errors are `z.core.$ZodError`, not `z.ZodError`. Adjust your `instanceof` checks.
69+
70+
## Formatting errors
71+
72+
### v4 — top-level functions
73+
74+
```ts
75+
import * as z from "zod";
76+
77+
const result = schema.safeParse(input);
78+
if (!result.success) {
79+
z.treeifyError(result.error); // nested object mirroring schema shape
80+
z.flattenError(result.error); // { formErrors, fieldErrors } — flat one-level shape
81+
z.prettifyError(result.error); // human-readable string with bullets and paths
82+
}
83+
```
84+
85+
`z.treeifyError(result.error)` for the example above returns:
86+
87+
```ts
88+
{
89+
errors: ["Unrecognized key: \"extraKey\""],
90+
properties: {
91+
username: { errors: ["Invalid input: expected string, received number"] },
92+
favoriteNumbers: {
93+
errors: [],
94+
items: [
95+
undefined,
96+
{ errors: ["Invalid input: expected number, received string"] },
97+
],
98+
},
99+
},
100+
}
101+
```
102+
103+
Access nested errors with optional chaining: `tree.properties?.username?.errors`.
104+
105+
`z.prettifyError(result.error)` returns:
106+
107+
```
108+
✖ Unrecognized key: "extraKey"
109+
✖ Invalid input: expected string, received number
110+
→ at username
111+
✖ Invalid input: expected number, received string
112+
→ at favoriteNumbers[1]
113+
```
114+
115+
`z.formatError(err)` still exists in v4 but is **deprecated** — prefer `z.treeifyError` (the shape changed slightly: `errors`/`properties`/`items` instead of v3's `_errors` underscore convention).
116+
117+
### v3 — instance methods
118+
119+
```ts
120+
// v3
121+
const result = schema.safeParse(input);
122+
if (!result.success) {
123+
result.error.format(); // { _errors, [field]: { _errors } }
124+
result.error.flatten(); // { formErrors, fieldErrors }
125+
}
126+
```
127+
128+
The v3 `format()` shape uses `_errors` as the leaf array on every node:
129+
130+
```ts
131+
{
132+
_errors: ["Unrecognized key: \"extraKey\""],
133+
username: { _errors: ["Invalid input: expected string, received number"] },
134+
favoriteNumbers: {
135+
_errors: [],
136+
"1": { _errors: ["Invalid input: expected number, received string"] },
137+
}
138+
}
139+
```
140+
141+
When porting v3 form-handling code to v4, the `_errors``errors`/`items` rename is the most common breakage. `flattenError` shape (`{ formErrors, fieldErrors }`) is unchanged.
142+
143+
## Customizing error messages
144+
145+
### v4 — unified `error` param
146+
147+
A single `error` option replaces v3's separate `message` and `errorMap`. It accepts a string or a function.
148+
149+
```ts
150+
// static string
151+
z.string({ error: "Bad!" });
152+
z.string().min(5, { error: "Too short!" });
153+
z.uuid({ error: "Bad UUID!" });
154+
z.array(z.string(), { error: "Not an array!" });
155+
156+
// shorthand: positional string
157+
z.string("Bad!");
158+
z.string().min(5, "Too short!");
159+
160+
// function form (the v4 "error map")
161+
z.string({ error: (iss) => iss.input === undefined ? "Required" : "Invalid" });
162+
163+
// inspect issue context
164+
z.string().min(5, {
165+
error: (iss) => {
166+
iss.code; // issue code
167+
iss.input; // the input value
168+
iss.path; // the path within the parent schema
169+
iss.minimum; // available because we're on .min()
170+
iss.inclusive;
171+
return `Must be ≥ ${iss.minimum} chars`;
172+
},
173+
});
174+
175+
// per-parse override
176+
schema.parse(input, { error: (iss) => "Custom message" });
177+
178+
// global override
179+
z.config({ customError: (iss) => iss.path.length === 0 ? "Top-level error" : undefined });
180+
```
181+
182+
Returning `undefined` from the function falls through to the next map in Zod's precedence chain (schema-level → parse-level → global → default). Use this to selectively override only certain issue codes.
183+
184+
### v3 — separate `message` / `errorMap`
185+
186+
```ts
187+
// v3
188+
z.string({ required_error: "Required", invalid_type_error: "Bad!" });
189+
z.string().min(5, { message: "Too short!" });
190+
191+
// v3 errorMap function
192+
z.string({
193+
errorMap: (iss, ctx) => {
194+
if (iss.code === "invalid_type") return { message: "Bad!" };
195+
return { message: ctx.defaultError };
196+
},
197+
});
198+
199+
// v3 per-parse
200+
schema.parse(input, { errorMap });
201+
202+
// v3 global
203+
z.setErrorMap(myErrorMap);
204+
```
205+
206+
v3 → v4 cookbook:
207+
208+
| v3 | v4 |
209+
| --- | --- |
210+
| `z.string({ required_error, invalid_type_error })` | `z.string({ error: (iss) => iss.input === undefined ? "Required" : "Bad" })` |
211+
| `z.string({ message: "..." })` | `z.string({ error: "..." })` |
212+
| `z.string({ errorMap: fn })` | `z.string({ error: fn })` (signature simplified to one arg `iss`) |
213+
| `z.setErrorMap(fn)` | `z.config({ customError: fn })` |
214+
| `(iss, ctx) => ({ message: ctx.defaultError })` | `(iss) => undefined` (returning `undefined` defers to default) |
215+
216+
## Common failure modes
217+
218+
1. **`Synchronous parsing not supported`** — schema has async checks; switch caller from `parse` to `parseAsync`.
219+
2. **`error.format` is not a function** — code on v4 still using v3 instance method; replace with `z.treeifyError(error)`.
220+
3. **`z.formatError` is deprecated** — switch to `z.treeifyError`.
221+
4. **`error instanceof z.ZodError === false`** in `zod/mini` — Mini parse errors are `z.core.$ZodError`. Use `error instanceof z.core.$ZodError` or import `ZodError` from the regular package.
222+
5. **Custom message ignored** — passing v3 `{ message: ... }` shape to a v4 schema. Use `error: ...`.
223+
6. **`required_error` / `invalid_type_error` not recognized** in v4 — replace with a function `error: (iss) => iss.input === undefined ? "Required" : "Bad"`.
224+
7. **`ctx.defaultError` undefined in error map** — v4 error maps return `undefined` to defer; there is no `ctx.defaultError`.
225+
8. **Strict object rejecting valid data with extra fields** — using `z.strictObject` (v4) or `.strict()` (v3). Switch to `.loose()` (v4) / `.passthrough()` (v3) or remove the strictness.
226+
227+
## Issue codes (high-level)
228+
229+
Both versions emit the same conceptual codes, with minor renames in v4. Common ones:
230+
231+
`invalid_type`, `unrecognized_keys`, `invalid_union`, `invalid_value` (v4) / `invalid_literal` (v3), `too_small`, `too_big`, `not_multiple_of`, `invalid_string` (v3), `custom`.
232+
233+
In v4, string format violations use `invalid_format` (e.g. failed `z.email`). In v3 they use `invalid_string` with a `validation` field.
234+
235+
<!--
236+
Source references:
237+
- https://github.com/colinhacks/zod/blob/v4.3.6/packages/docs/content/basics.mdx
238+
- https://github.com/colinhacks/zod/blob/v4.3.6/packages/docs/content/error-formatting.mdx
239+
- https://github.com/colinhacks/zod/blob/v4.3.6/packages/docs/content/error-customization.mdx
240+
- https://github.com/colinhacks/zod/blob/v4.3.6/packages/docs/content/api.mdx
241+
- https://github.com/colinhacks/zod/tree/v3.25.76/packages/docs/content
242+
- https://github.com/colinhacks/zod/blob/v3.25.76/packages/docs/content/error-customization.mdx
243+
-->

0 commit comments

Comments
 (0)