Zod validation
Validate runtime data with Zod, then keep the TypeScript type in sync
TypeScript catches mistakes before code runs. Zod catches bad values after data crosses a runtime boundary: API responses, forms, env vars, webhooks, local storage, and message queues.
Start with the boundary, not the library
Zod is most valuable where your app stops controlling the shape of data. A fetch response, a form submission, a webhook payload, and an env var all look typed in your editor only after you prove they are valid at runtime.
- • Use Zod for untrusted runtime input.
- • Skip Zod for values already created by your own typed code.
- • Generate a first draft when you have JSON, then tighten rules by hand.
Generate the first schema from real JSON
A generated schema is a starting point, not a contract. Use a real payload with nested objects, arrays, nulls, and optional-looking fields. If the API returns multiple variants, paste multiple samples into the JSON to Zod converter and review optional fields carefully.
import { z } from "zod";
const UserSchema = z.object({
id: z.number().int(),
name: z.string(),
email: z.string().email().optional(),
roles: z.array(z.string()),
});
type User = z.infer<typeof UserSchema>;Use safeParse for recoverable errors
`parse()` throws. That is useful when invalid data should stop the flow. `safeParse()` returns a result object, which is better for UI validation, logging, and graceful fallback.
const result = UserSchema.safeParse(await response.json());
if (!result.success) {
console.error(result.error.flatten());
throw new Error("API returned an unexpected user shape");
}
const user = result.data;Validate env vars before the app boots
Env vars are strings at runtime, and missing values often fail late. A small Zod schema makes configuration errors loud during startup.
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().min(1),
ENABLE_BILLING: z.enum(["true", "false"]).default("false"),
});
export const env = EnvSchema.parse(process.env);Common mistakes
One JSON sample proves what appeared once. It does not prove which fields can be omitted, null, or variant-shaped in production.
Validate at the boundary, then pass typed data inward. Revalidating every internal function call adds noise without much safety.
Generation can infer strings and numbers. You still need to add rules like `.email()`, `.url()`, `.min()`, `.int()`, and domain constraints.