From zod-consistency
Write, review, refactor, or debug TypeScript validation code with Zod (z.object schemas, safeParse, z.infer, refinements, transforms, form and API validation) using one canonical idiom set. Use this skill whenever code validates request bodies, env vars, form input, or API responses with Zod, derives types from schemas, or when the user hits hand-written interfaces drifting from schemas, uncaught ZodError crashes, unknown keys silently disappearing, optional vs nullable confusion, or transform/refine ordering surprises. Trigger it even when the user just says "validate this payload" or "type this API response" in a TypeScript project using Zod — without saying the words "Zod idioms."
How this skill is triggered — by the user, by Claude, or both
Slash command
/zod-consistency:zod-consistencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Zod is stable, but generated code drifts in predictable ways: a hand-written `interface`
Zod is stable, but generated code drifts in predictable ways: a hand-written interface
duplicating (and diverging from) each schema, parse in try/catch where safeParse
belongs, copy-pasted near-identical schemas instead of composition, and confusion among
optional/nullable/default and strip/strict semantics. This skill pins one canonical
idiom set for Zod 3.x.
| Always | Never | Why |
|---|---|---|
type User = z.infer<typeof UserSchema> | a parallel hand-written interface User | Two sources of truth drift; the schema is the type. |
const r = Schema.safeParse(data); if (!r.success) ... at boundaries | try { Schema.parse(data) } catch ... for expected-invalid input | Invalid input isn't exceptional at a boundary; safeParse returns a discriminated union you must handle. |
.extend() / .pick() / .omit() / .partial() to derive schemas | re-declaring overlapping schemas | Composition keeps one canonical definition per concept. |
know the default: z.object strips unknown keys; .strict() to reject, .passthrough() to keep | assuming extra keys survive (or get rejected) | Stripping silently drops data; rejecting unexpectedly 400s — choose per boundary. |
z.coerce.number() for query params/env vars | Number(x) casts before validation | Coercion happens inside the schema with proper failure reporting. |
z.discriminatedUnion("type", [...]) for tagged data | z.union over object shapes with a tag | Discriminated unions give exact branch errors and faster checks; plain unions report a wall of every-branch failures. |
.optional() = may be undefined/absent; .nullable() = may be null; .default(v) = absent → v | treating them as interchangeable | JSON null vs missing key are different wire facts; APIs distinguish them. |
.refine/.superRefine for cross-field rules on the object | validating relationships in app code after parse | The schema should encode the full contract; post-hoc checks get skipped. |
name exported schemas like types: export const OrderSchema = z.object({...}) | inline anonymous mega-schemas at call sites | Reuse and inference need named schemas. |
validate env once at startup: EnvSchema.parse(process.env) (crash early is right here) | process.env.X! scattered non-null assertions | One parse, typed config object, immediate failure on misconfiguration. |
House style:
import { z } from "zod";
export const OrderSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
quantity: z.coerce.number().int().positive(),
couponCode: z.string().min(3).optional(),
shippedAt: z.coerce.date().nullable(), // wire sends null until shipped
});
export type Order = z.infer<typeof OrderSchema>;
export function parseOrder(input: unknown): Order | { errors: string[] } {
const result = OrderSchema.safeParse(input);
if (!result.success) {
return { errors: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`) };
}
return result.data;
}
.passthrough() for forwarding shapes; .strict() when
unknown keys indicate client bugs..transform changes the output type: a schema with transforms has
z.input<> ≠ z.output<>; using z.infer (= output) where the input shape is
meant (e.g., form initial values) mistypes silently..refine runs after the base type check but schemaA.or(schemaB)
short-circuits; string().transform(s => s.trim()).pipe(z.string().min(1)) is the
validate-after-transform idiom — .min(1).transform(trim) accepts " "..merge vs .extend: merge takes another schema's full config (and the last
unknown-keys policy wins); extend adds/overrides fields. Mostly you want extend..partial() is shallow — nested objects stay required; .deepPartial() existed
but is deprecated — model nested-optional explicitly.z.enum vs z.nativeEnum: literal string arrays (z.enum(["a", "b"])) vs TS
enum values; feeding a TS enum to z.enum type-errors or worse.error.issues (each with path, code, message) is the data;
error.flatten() is for form field-mapping (fieldErrors/formErrors); custom
messages go in the check itself (z.string().min(1, "Required")).safeParseAsync/parseAsync required when any .refine is async — the sync
versions throw at runtime, in the unhappy path only.z.date() rejects ISO strings — use
z.coerce.date() (or z.string().datetime()) for wire formats.Target Zod 3.x (the long-stable line). Zod 4 (and the zod/v4 subpath in late 3.x)
renames internals — notably error customization (error: replacing
message:/errorMap), z.strictObject/looseObject sugar, and treeifyError replacing
some flatten use — but the idioms above carry over. Match whichever major the project
locks; never mix v3 and v4 import styles in one codebase.
pick/omit/extend/partial), and types by z.infer only.safeParse; map issues into your error shape;
inside the boundary, trust the inferred types — no re-checking.process.env raw access.For the composition catalog, transform/pipe semantics, error-shaping recipes, and
ecosystem touchpoints (forms, OpenAPI), read references/zod-patterns.md.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub guidogl/zod-consistency --plugin zod-consistency