From cf35f490f5e7c4aad7d46b80391d6b51ac6cc0c2 Mon Sep 17 00:00:00 2001 From: sx4im Date: Thu, 5 Mar 2026 22:40:05 +0500 Subject: [PATCH] feat(skills): add zod-validation-expert skill for type-safe schema definitions and parsing --- skills/zod-validation-expert/SKILL.md | 267 ++++++++++++++++++++++++++ skills_index.json | 10 + 2 files changed, 277 insertions(+) create mode 100644 skills/zod-validation-expert/SKILL.md diff --git a/skills/zod-validation-expert/SKILL.md b/skills/zod-validation-expert/SKILL.md new file mode 100644 index 00000000..cfac49a3 --- /dev/null +++ b/skills/zod-validation-expert/SKILL.md @@ -0,0 +1,267 @@ +--- +name: zod-validation-expert +description: "Expert in Zod — TypeScript-first schema validation. Covers parsing, custom errors, refinements, type inference, and integration with React Hook Form, Next.js, and tRPC." +risk: safe +source: community +date_added: "2026-03-05" +--- + +# Zod Validation Expert + +You are a production-grade Zod expert. You help developers build type-safe schema definitions and validation logic. You master Zod fundamentals (primitives, objects, arrays, records), type inference (`z.infer`), complex validations (`.refine`, `.superRefine`), transformations (`.transform`), and integrations across the modern TypeScript ecosystem (React Hook Form, Next.js API Routes / App Router Actions, tRPC, and environment variables). + +## When to Use This Skill + +- Use when defining TypeScript validation schemas for API inputs or forms +- Use when setting up environment variable validation (`process.env`) +- Use when integrating Zod with React Hook Form (`@hookform/resolvers/zod`) +- Use when extracting or inferring TypeScript types from runtime validation schemas +- Use when writing complex validation rules (e.g., cross-field validation, async validation) +- Use when transforming input data (e.g., string to Date, string to number coercion) +- Use when standardizing error message formatting + +## Core Concepts + +### Why Zod? + +Zod eliminates the duplication of writing a TypeScript interface *and* a runtime validation schema. You define the schema once, and Zod infers the static TypeScript type. Note that Zod is for **parsing, not just validation**. `safeParse` and `parse` return clean, typed data, stripping out unknown keys by default. + +## Schema Definition & Inference + +### Primitives & Coercion + +```typescript +import { z } from "zod"; + +// Basic primitives +const stringSchema = z.string().min(3).max(255); +const numberSchema = z.number().int().positive(); +const dateSchema = z.date(); + +// Coercion (automatically casting inputs before validation) +// Highly useful for FormData in Next.js Server Actions or URL queries +const ageSchema = z.coerce.number().min(18); // "18" -> 18 +const activeSchema = z.coerce.boolean(); // "true" -> true +const dobSchema = z.coerce.date(); // "2020-01-01" -> Date object +``` + +### Objects & Type Inference + +```typescript +const UserSchema = z.object({ + id: z.string().uuid(), + username: z.string().min(3).max(20), + email: z.string().email(), + role: z.enum(["ADMIN", "USER", "GUEST"]).default("USER"), + age: z.number().min(18).optional(), // Can be omitted + website: z.string().url().nullable(), // Can be null + tags: z.array(z.string()).min(1), // Array with at least 1 item +}); + +// Infer the TypeScript type directly from the schema +// No need to write a separate `interface User { ... }` +export type User = z.infer; +``` + +### Advanced Types + +```typescript +// Records (Objects with dynamic keys but specific value types) +const envSchema = z.record(z.string(), z.string()); // Record + +// Unions (OR) +const idSchema = z.union([z.string(), z.number()]); // string | number +// Or simpler: +const idSchema2 = z.string().or(z.number()); + +// Discriminated Unions (Type-safe switch cases) +const ActionSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("create"), id: z.string() }), + z.object({ type: z.literal("update"), id: z.string(), data: z.any() }), + z.object({ type: z.literal("delete"), id: z.string() }), +]); +``` + +## Parsing & Validation + +### parse vs safeParse + +```typescript +const schema = z.string().email(); + +// ❌ parse: Throws a ZodError if validation fails +try { + const email = schema.parse("invalid-email"); +} catch (err) { + if (err instanceof z.ZodError) { + console.error(err.issues); + } +} + +// ✅ safeParse: Returns a result object (No try/catch needed) +const result = schema.safeParse("user@example.com"); + +if (!result.success) { + // TypeScript narrows result to SafeParseError + console.log(result.error.format()); + // Early return or throw domain error +} else { + // TypeScript narrows result to SafeParseSuccess + const validEmail = result.data; // Type is `string` +} +``` + +## Customizing Validation + +### Custom Error Messages + +```typescript +const passwordSchema = z.string() + .min(8, { message: "Password must be at least 8 characters long" }) + .max(100, { message: "Password is too long" }) + .regex(/[A-Z]/, { message: "Password must contain at least one uppercase letter" }) + .regex(/[0-9]/, { message: "Password must contain at least one number" }); + +// Global custom error map (useful for i18n) +z.setErrorMap((issue, ctx) => { + if (issue.code === z.ZodIssueCode.invalid_type) { + if (issue.expected === "string") return { message: "This field must be text" }; + } + return { message: ctx.defaultError }; +}); +``` + +### Refinements (Custom Logic) + +```typescript +// Basic refinement +const passwordCheck = z.string().refine((val) => val !== "password123", { + message: "Password is too weak", +}); + +// Cross-field validation (e.g., password matching) +const formSchema = z.object({ + password: z.string().min(8), + confirmPassword: z.string() +}).refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], // Sets the error on the specific field +}); +``` + +### Transformations + +```typescript +// Change data during parsing +const stringToNumber = z.string() + .transform((val) => parseInt(val, 10)) + .refine((val) => !isNaN(val), { message: "Not a valid integer" }); + +// Now the inferred type is `number`, not `string`! +type TransformedResult = z.infer; // number +``` + +## Integration Patterns + +### React Hook Form + +```typescript +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +const loginSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be 6+ characters"), +}); + +type LoginFormValues = z.infer; + +export function LoginForm() { + const { register, handleSubmit, formState: { errors } } = useForm({ + resolver: zodResolver(loginSchema) + }); + + const onSubmit = (data: LoginFormValues) => { + // data is fully typed and validated + console.log(data.email, data.password); + }; + + return ( +
+ + {errors.email && {errors.email.message}} + {/* ... */} +
+ ); +} +``` + +### Next.js Server Actions + +```typescript +"use server"; +import { z } from "zod"; + +// Coercion is critical here because FormData values are always strings +const createPostSchema = z.object({ + title: z.string().min(3), + content: z.string().optional(), + published: z.coerce.boolean().default(false), // checkbox -> "on" -> true +}); + +export async function createPost(prevState: any, formData: FormData) { + // Convert FormData to standard object using Object.fromEntries + const rawData = Object.fromEntries(formData.entries()); + + const validatedFields = createPostSchema.safeParse(rawData); + + if (!validatedFields.success) { + return { + errors: validatedFields.error.flatten().fieldErrors, + }; + } + + // Proceed with validated database operation + const { title, content, published } = validatedFields.data; + // ... + return { success: true }; +} +``` + +### Environment Variables + +```typescript +// Make environment variables strictly typed and fail-fast +import { z } from "zod"; + +const envSchema = z.object({ + DATABASE_URL: z.string().url(), + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + PORT: z.coerce.number().default(3000), + API_KEY: z.string().min(10), +}); + +// Fails the build immediately if env vars are missing or invalid +const env = envSchema.parse(process.env); + +export default env; +``` + +## Best Practices + +- ✅ **Do:** Co-locate schemas alongside the components or API routes that use them to maintain separation of concerns. +- ✅ **Do:** Use `z.infer` everywhere instead of maintaining duplicate TypeScript interfaces manually. +- ✅ **Do:** Prefer `safeParse` over `parse` to avoid scattered `try/catch` blocks and leverage TypeScript's control flow narrowing for robust error handling. +- ✅ **Do:** Use `z.coerce` when accepting data from `URLSearchParams` or `FormData`, and be aware that `z.coerce.boolean()` converts standard `"false"`/`"off"` strings unexpectedly without custom preprocessing. +- ✅ **Do:** Use `.flatten()` or `.format()` on `ZodError` objects to easily extract serializable, human-readable errors for frontend consumption. +- ❌ **Don't:** Rely exclusively on `.partial()` for update schemas if field types or constraints differ between creation and update operations; define distinct schemas instead. +- ❌ **Don't:** Forget to pass the `path` option in `.refine()` or `.superRefine()` when performing object-level cross-field validations, otherwise the error won't attach to the correct input field. + +## Troubleshooting + +**Problem:** `Type instantiation is excessively deep and possibly infinite.` +**Solution:** This occurs with extreme schema recursion (e.g. deeply nested self-referential schemas). Use `z.lazy(() => NodeSchema)` for recursive structures and define the base TypeScript type explicitly instead of solely inferring it. + +**Problem:** Empty strings pass validation when using `.optional()`. +**Solution:** `.optional()` permits `undefined`, not empty strings. If an empty string means "no value," use `.or(z.literal(""))` or preprocess it: `z.string().transform(v => v === "" ? undefined : v).optional()`. diff --git a/skills_index.json b/skills_index.json index a54ff340..28412fec 100644 --- a/skills_index.json +++ b/skills_index.json @@ -10029,6 +10029,16 @@ "source": "community", "date_added": "2026-02-27" }, + { + "id": "zod-validation-expert", + "path": "skills/zod-validation-expert", + "category": "uncategorized", + "name": "zod-validation-expert", + "description": "Expert in Zod \u2014 TypeScript-first schema validation. Covers parsing, custom errors, refinements, type inference, and integration with React Hook Form, Next.js, and tRPC.", + "risk": "safe", + "source": "community", + "date_added": "2026-03-05" + }, { "id": "zoho-crm-automation", "path": "skills/zoho-crm-automation",