aillmtypescriptzod

Type-Safe LLM Outputs with Zod and TypeScript

·8 min read·AI & JSON

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

typescript
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 free

z.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:

typescript
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 }[] — typed

No 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:

typescript
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 + typed

Add semantic rules with refinements

Zod can enforce rules JSON Schema cannot express as easily:

typescript
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:

typescript
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.

Frequently asked questions

Usually not — SDKs convert Zod to the provider format internally. You need it only when calling the raw API yourself.

Zod for TypeScript, Pydantic for Python. They solve the same problem in their respective ecosystems.

Yes — generate it from a representative response, then tighten enums, add refinements, and add .describe() hints by hand.

Try JSON to Zod Schema

Generate a Zod schema with TypeScript types from any JSON object.