Zod
Define and validate data schemas with Zod for parsing, transforms, refinements,
You are an expert at data validation with Zod. You define schemas that parse unknown data into typed values, compose complex schemas from primitives, apply transforms and refinements, and integrate schemas with form libraries and API layers for end-to-end type safety.
## Key Points
- **Casting with `as Type` instead of parsing**. `json as User` bypasses all validation; `userSchema.parse(json)` guarantees correctness.
- **Maintaining separate interfaces and schemas**. Use `z.infer<typeof schema>` as the type. One source of truth, zero drift.
- **Using `.parse()` when you should use `.safeParse()`**. In request handlers, `.safeParse()` lets you return 400 errors; `.parse()` throws unhandled exceptions.
- **Deeply nesting `.refine()` inside object fields** when `.superRefine()` at the object level is clearer for cross-field validation.
- Validating API request/response payloads at runtime with full TypeScript inference.
- Defining form validation schemas shared between client and server (with React Hook Form or Formik).
- Parsing environment variables, config files, or CLI arguments at startup.
- Building tRPC procedures where Zod schemas define input/output contracts.
- Replacing manual type guards and assertion functions with declarative schemas.
## Quick Example
```typescript
// npm install zod
import { z, ZodError } from "zod";
```skilldb get form-survey-services-skills/ZodFull skill: 288 linesZod Schema Validation Skill
You are an expert at data validation with Zod. You define schemas that parse unknown data into typed values, compose complex schemas from primitives, apply transforms and refinements, and integrate schemas with form libraries and API layers for end-to-end type safety.
Core Philosophy
Parse, Don't Validate
Zod schemas are parsers, not validators. Call .parse() or .safeParse() on untrusted data and receive either a fully typed result or structured errors. Never trust runtime data — parse it at system boundaries (API routes, form submissions, environment variables, config files).
Single Source of Truth
Define the Zod schema once and infer the TypeScript type with z.infer<>. Never maintain a separate interface that duplicates the schema. When the schema changes, types update automatically. This eliminates the entire category of type-vs-validation drift bugs.
Composition Over Complexity
Build complex schemas by composing small, reusable schemas with .extend(), .merge(), .pick(), .omit(), and .intersection(). Keep individual schemas focused on one concern. A userSchema composed from emailSchema and nameSchema is easier to test than a monolithic object.
Setup
Install Zod:
// npm install zod
import { z, ZodError } from "zod";
No environment variables needed. Zod is a pure runtime library with zero dependencies.
Key Patterns
Parse Unknown Data at Boundaries
Do this — parse and narrow types at API boundaries:
const userResponseSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
createdAt: z.string().datetime(),
metadata: z.record(z.string(), z.unknown()).optional(),
});
type UserResponse = z.infer<typeof userResponseSchema>;
// At the API boundary — parse the response
async function fetchUser(id: string): Promise<UserResponse> {
const res = await fetch(`/api/users/${id}`);
const json = await res.json();
return userResponseSchema.parse(json); // throws ZodError if invalid
}
// Safe version that returns a discriminated union
async function fetchUserSafe(id: string) {
const res = await fetch(`/api/users/${id}`);
const json = await res.json();
const result = userResponseSchema.safeParse(json);
if (!result.success) {
console.error("Validation failed:", result.error.flatten());
return null;
}
return result.data; // fully typed UserResponse
}
Not this — casting as UserResponse on unvalidated API responses and hoping the shape is correct.
Transforms and Preprocessing
Do this — use transforms to coerce and reshape data:
// Coerce strings to proper types (common with form data and query params)
const searchParamsSchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(["asc", "desc"]).default("desc"),
q: z.string().trim().optional(),
});
// Transform the parsed shape
const apiDateSchema = z.string().datetime().transform((s) => new Date(s));
const eventSchema = z.object({
title: z.string().trim().min(1),
startsAt: apiDateSchema,
endsAt: apiDateSchema,
}).refine((data) => data.endsAt > data.startsAt, {
message: "End date must be after start date",
path: ["endsAt"],
});
type Event = z.infer<typeof eventSchema>;
// { title: string; startsAt: Date; endsAt: Date }
// Preprocess to handle null/undefined before validation
const nullableNumberSchema = z.preprocess(
(val) => (val === "" || val === null ? undefined : val),
z.number().optional()
);
Not this — parsing dates or coercing numbers manually after validation with ad-hoc utility functions.
Refinements for Custom Validation
Do this — add custom rules with .refine() and .superRefine():
const passwordSchema = z
.string()
.min(8, "At least 8 characters")
.refine((pw) => /[A-Z]/.test(pw), "Must contain an uppercase letter")
.refine((pw) => /[0-9]/.test(pw), "Must contain a number")
.refine((pw) => /[^A-Za-z0-9]/.test(pw), "Must contain a special character");
const registrationSchema = z.object({
email: z.string().email(),
password: passwordSchema,
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
// superRefine for multiple conditional errors
const teamSchema = z.object({
name: z.string().min(1),
members: z.array(z.string().email()),
plan: z.enum(["free", "pro", "enterprise"]),
}).superRefine((data, ctx) => {
const maxMembers = { free: 3, pro: 20, enterprise: Infinity }[data.plan];
if (data.members.length > maxMembers) {
ctx.addIssue({
code: z.ZodIssueCode.too_big,
maximum: maxMembers,
inclusive: true,
type: "array",
path: ["members"],
message: `${data.plan} plan allows max ${maxMembers} members`,
});
}
});
Not this — validating cross-field rules in submit handlers instead of in the schema where they belong.
Common Patterns
Schema Composition
const addressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
state: z.string().length(2),
zip: z.string().regex(/^\d{5}(-\d{4})?$/, "Invalid ZIP code"),
});
const baseUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
// Extend, pick, omit
const fullUserSchema = baseUserSchema.extend({
address: addressSchema,
role: z.enum(["admin", "user"]),
});
const createUserSchema = fullUserSchema.omit({ role: true });
const updateUserSchema = fullUserSchema.partial().required({ email: true });
type CreateUser = z.infer<typeof createUserSchema>;
type UpdateUser = z.infer<typeof updateUserSchema>;
Environment Variable Validation
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url().optional(),
PORT: z.coerce.number().int().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
API_SECRET: z.string().min(32),
});
export const env = envSchema.parse(process.env);
// Fully typed: env.DATABASE_URL is string, env.PORT is number
React Hook Form Integration
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const formSchema = z.object({
title: z.string().min(1, "Title is required"),
body: z.string().min(10, "Body must be at least 10 characters"),
tags: z.array(z.string()).min(1, "Add at least one tag"),
});
type FormData = z.infer<typeof formSchema>;
function PostForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("title")} />
{errors.title && <span>{errors.title.message}</span>}
<textarea {...register("body")} />
{errors.body && <span>{errors.body.message}</span>}
<button type="submit">Publish</button>
</form>
);
}
Discriminated Unions
const shapeSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("circle"), radius: z.number().positive() }),
z.object({ type: z.literal("rectangle"), width: z.number().positive(), height: z.number().positive() }),
z.object({ type: z.literal("triangle"), base: z.number().positive(), height: z.number().positive() }),
]);
type Shape = z.infer<typeof shapeSchema>;
function area(shape: Shape): number {
switch (shape.type) {
case "circle": return Math.PI * shape.radius ** 2;
case "rectangle": return shape.width * shape.height;
case "triangle": return 0.5 * shape.base * shape.height;
}
}
Formatted Error Messages
function formatZodErrors(error: ZodError): Record<string, string> {
const formatted: Record<string, string> = {};
for (const issue of error.issues) {
const path = issue.path.join(".");
if (!formatted[path]) {
formatted[path] = issue.message;
}
}
return formatted;
}
// Or use built-in flatten
const result = userSchema.safeParse(input);
if (!result.success) {
const flat = result.error.flatten();
// flat.fieldErrors: { email?: string[], name?: string[] }
// flat.formErrors: string[]
}
Anti-Patterns
- Casting with
as Typeinstead of parsing.json as Userbypasses all validation;userSchema.parse(json)guarantees correctness. - Maintaining separate interfaces and schemas. Use
z.infer<typeof schema>as the type. One source of truth, zero drift. - Using
.parse()when you should use.safeParse(). In request handlers,.safeParse()lets you return 400 errors;.parse()throws unhandled exceptions. - Deeply nesting
.refine()inside object fields when.superRefine()at the object level is clearer for cross-field validation.
When to Use
- Validating API request/response payloads at runtime with full TypeScript inference.
- Defining form validation schemas shared between client and server (with React Hook Form or Formik).
- Parsing environment variables, config files, or CLI arguments at startup.
- Building tRPC procedures where Zod schemas define input/output contracts.
- Replacing manual type guards and assertion functions with declarative schemas.
Install this skill directly: skilldb add form-survey-services-skills
Related Skills
Formbricks
Integrate Formbricks open-source surveys for in-app, website, and link-based
Formik
Build React forms with Formik using useFormik hook, Field components, and
Jotform
Integrate JotForm's REST API to create forms, retrieve submissions, process
React Hook Form
Build performant React forms with React Hook Form using register, validation,
Surveymonkey
Integrate SurveyMonkey's REST API to create surveys, collect responses via
Tally
Integrate Tally forms via its API and embed SDK to handle submissions, configure