One schema, three jobs
In a TypeScript AI app you typically need three things: a schema to send the model, validation of its response, and a static type for the parsed object. Writing those separately invites drift. Zod lets you write one schema that does all three — and modern AI SDKs accept Zod schemas directly as the model's output contract.
Define the schema
import { z } from "zod";
const Recipe = z.object({
title: z.string(),
servings: z.number().int().positive(),
ingredients: z.array(z.object({
name: z.string(),
quantity: z.string(),
})),
difficulty: z.enum(["easy", "medium", "hard"]),
});
type Recipe = z.infer<typeof Recipe>; // static type, for freez.infer derives the TypeScript type from the schema, so the type and the validator can never disagree.
Use it as the model's output contract
AI SDKs that support structured generation take the Zod schema, convert it to JSON Schema for the provider, and return a parsed, validated object:
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";
const { object } = await generateObject({
model: openai("gpt-4.1"),
schema: Recipe,
prompt: "Give me a simple dal recipe for 4 people.",
});
object.title; // string — typed
object.ingredients; // { name: string; quantity: string }[] — typedNo JSON.parse, no manual interface, no casting. If the model's output does not satisfy the schema, the SDK throws.
Manual pipeline (without an SDK)
If you call the API directly, convert the Zod schema to JSON Schema for the request, then validate the response:
import { zodToJsonSchema } from "zod-to-json-schema";
const jsonSchema = zodToJsonSchema(Recipe, "Recipe");
// ...send jsonSchema in response_format / tool input_schema...
const parsed = Recipe.parse(JSON.parse(rawResponse)); // validated + typedAdd semantic rules with refinements
Zod can enforce rules JSON Schema cannot express as easily:
const Booking = z.object({
start: z.string().datetime(),
end: z.string().datetime(),
}).refine(b => new Date(b.end) > new Date(b.start), {
message: "end must be after start",
});These checks run when you parse, so impossible data from the model is rejected with a clear message you can feed back for a retry.
Describe fields to guide the model
Zod .describe() calls become description entries in the generated JSON Schema, which the model reads:
const Ticket = z.object({
priority: z.enum(["low", "high"]).describe("Use 'high' only for outages."),
});Why this pattern wins
- Single source of truth — schema, type, and validation never drift.
- End-to-end type safety — the parsed object is fully typed downstream.
- Clear failures — Zod errors pinpoint exactly which field was wrong, perfect for re-prompting.