---
title: "Structured outputs with Zod"
summary: "generateText with Output.object enforces a Zod schema so the model returns JSON you can trust in TypeScript."
track: "The model"
day: 4
minutes: 18
author: "Akash Panchal"
url: https://ai-sdk-patterns.dev/learn/fundamentals/day-4/structured-json
dateModified: 2026-08-29
---

# Structured outputs with Zod

generateText with Output.object enforces a Zod schema so the model returns JSON you can trust in TypeScript.

*The model · Day 4: Structured output · ~18 min. Written by [Akash Panchal](https://github.com/akashp1712).*

Canonical: https://ai-sdk-patterns.dev/learn/fundamentals/day-4/structured-json

## After this topic

You will stop asking the model for “JSON only” and hoping. You will have a Zod schema, a generateText call with Output.object, and a typed object you can save to a database — or a loud failure if the model cannot fill the shape.

## Likely text is not a contract

> The model writes likely text. “Looks like JSON” is not the same as “is JSON your TypeScript can trust.”

If the next line of code needs fields — name, role, a list of tasks — you need a schema, not a prompt that says “return JSON”.

You already know the model writes likely text. If you ask it for JSON in the prompt — “return JSON only” — you still get likely text. Often that is JSON. Often it is JSON wrapped in markdown fences. Often it has a preface (“Sure, here is…”). JSON.parse throws. Your form never fills.

Production features that need data — a profile object, a list of tasks, a routing decision — cannot afford a string you hope is JSON. They need a contract: these keys, these types, or fail loud.

This is the same Day 1 lesson with a new surface. Constraining the guess is your job. A schema is a constraint the SDK can enforce. A sentence in the prompt is a suggestion the model can ignore.

### do-not.ts

```ts
const { text } = await generateText({
  model: getModel(),
  prompt: "Return JSON with name and role. JSON only.",
});

const data = JSON.parse(text);
// Often throws: the model added markdown fences or a preface.
```

- **JSON.parse on a guess.** The model often wraps JSON in ```json fences or adds a sentence. That throws. Output.object exists so you do not police that.

## generateText + Output.object

In AI SDK v6, structured data is still generateText. You pass an output option, not a separate generateObject call (that API is gone). Output.object takes a Zod schema. You read result.output — parsed, typed, or the call failed.

The schema is the shape. The prompt is the instruction. Keep both small. A 40-field object will be slow, expensive, and more likely to fail validation. Split work across steps if you must.

Providers that support native structured output will use it. Providers that do not still get a validated object — the SDK retries or repairs according to the current docs. You write one call either way.

### example.ts

```ts
import { generateText, Output } from "ai";
import { z } from "zod";
import { getModel } from "@/lib/model";

const profileSchema = z.object({
  name: z.string(),
  role: z.string(),
  skills: z.array(z.string()),
});

const { output } = await generateText({
  model: getModel(),
  output: Output.object({ schema: profileSchema }),
  prompt: "Generate a profile for a TypeScript AI engineer.",
});
```

- **Zod is the contract.** z.object({…}) is runtime validation and TypeScript types. The model is steered to fill those fields. You do not regex the keys out.
- **output, not object.** v6 returns { output } from generateText. If the model cannot satisfy the schema, the SDK fails loud — better than a half-valid string in production.
- **prompt is still just text.** The schema does not replace the prompt. It constrains the completion. Say what you want; the schema says what shape it must have.

## When the next line needs fields

Use Output.object when code will read properties: save to Postgres, render a card, pick a branch, call another API. The user never sees the JSON. They see a form that filled itself, or a ticket that was created.

Use plain generateText or streamText when the user is meant to read an essay. Streaming a half-object onto the screen is worse than waiting for a valid one.

Enums and small unions are gold. z.enum(['billing', 'bug', 'other']) as a routing decision is a better product than a free-text “category” you then parse with more AI.

- Data the code will use — Output.object
- Prose the user will read — generateText / streamText
- A choice among known options — z.enum, not a paragraph

## Trade-offs

Larger schemas cost more tokens and more latency. Prefer the smallest object that unblocks the next step. If validation keeps failing, the prompt is unclear or the schema is too tight — fix those before retrying forever.

Nested objects and long arrays are where models get sloppy. Flatten. Cap array length in the schema. Two small generateText calls beat one giant blob.

Failures should be loud. Catch them, show the user a retry, log the raw error. Do not JSON.parse a consolation string “just in case.” That is how you ship a profile named ```json.

## Common questions

### What is “Structured outputs with Zod”?

generateText with Output.object enforces a Zod schema so the model returns JSON you can trust in TypeScript.

### What will I be able to do after this lesson?

You will stop asking the model for “JSON only” and hoping. You will have a Zod schema, a generateText call with Output.object, and a typed object you can save to a database — or a loud failure if the model cannot fill the shape.

### How long does this lesson take?

About 18 minutes of reading. It is a free chapter in the AI SDK Patterns TypeScript course.

### When should I not use this?

Larger schemas cost more tokens and more latency. Prefer the smallest object that unblocks the next step. If validation keeps failing, the prompt is unclear or the schema is too tight — fix those before retrying forever.
