Skip to main content
Getting Typed JSON Out of LLMs: How We Use generateObject and the AI SDK

Getting Typed JSON Out of LLMs: How We Use generateObject and the AI SDK

August 1, 2026
AI Engineering
6 min read

Key takeaways

The Vercel AI SDK's `generateObject` function returns a typed object validated against a Zod or JSON schema; on a schema mismatch it throws instead of handing our code bad data.

`streamObject` streams a partial object through `partialObjectStream`, so a form or table fills in field-by-field before the model finishes.

The AI SDK ships four output modes: `object` (the default), `array` (streams elements through `elementStream`), `enum` (single-label classification), and `no-schema` (freeform JSON).

We use `generateObject` to extract data and tool calling to take actions; `experimental_output` combines a tool-calling loop with a final typed object.

When a model returns invalid JSON the SDK throws `NoObjectGeneratedError`, which carries the raw text and token usage; `experimental_repairText` and tighter schema descriptions recover most of those cases.

What does generateObject actually do?

`generateObject` is a Vercel AI SDK function that forces a language model to return JSON matching a schema we define, and validates the response before our code ever sees it. We pass a model, a Zod schema, and a prompt, and we get back a typed object whose shape TypeScript already knows.

In practice we call `generateObject` with a model string such as `anthropic/claude-sonnet-5`, a `z.object({ ... })` schema describing the fields we want — a title, a bounded array of tags, a sentiment enum — and the source text in the prompt. The returned `object.sentiment` is typed as the exact union we declared.

The schema does double duty: it steers the model toward the right shape, and it validates the output. If the model returns a field that does not parse, `generateObject` throws rather than returning half-valid data, so the object we get back is guaranteed to match the schema or we never get it at all.

Why not just parse JSON from generateText?

Because calling `JSON.parse` on a `generateText` string is exactly the failure mode `generateObject` exists to remove. A raw completion is a string that is well-formed JSON most of the time — until the model wraps it in a markdown fence, adds a trailing comment, or drops a required field, and the parse throws in production.

`generateObject` injects the schema into the request, uses each provider's structured-output or tool machinery to constrain generation, and validates the result with the Zod schema before returning. The win is not fewer characters of code; it is that the boundary between model output and typed value has one owner — the schema — instead of being smeared across a prompt, a regex, and a try/catch.

When should we use streamObject instead of generateObject?

We reach for `streamObject` when the object is big enough that waiting for the whole thing feels slow. `streamObject` returns a `partialObjectStream` that yields the object as it is built, so the UI can render fields the moment they arrive instead of blocking on the final token.

Each emitted value is a deep-partial of the schema, so every field can be undefined until the model fills it. We render with that assumption and never index into a nested field without a guard. For one-shot server work — a cron job or a route handler that returns once — `generateObject` is simpler and stays our default; `streamObject` is specifically for user-facing latency.

What output modes does the AI SDK support?

The AI SDK exposes four output strategies, and picking the right one removes a lot of downstream branching. `object` (the default) returns one validated object and suits extraction and summarization. `array` streams elements through `elementStream` for lists where each row can render as it lands. `enum` returns one string from a fixed set for classification and routing. `no-schema` returns arbitrary parsed JSON for exploratory prompts where the shape is unknown.

For `enum` we pass `output: 'enum'` and a list such as `['spam', 'not_spam']`; the model can only return one of those exact strings, which is stricter and cheaper than an object with one enum field. For `array` we describe a single element in the schema and consume `elementStream` to render rows as they complete rather than waiting for the full list.

When do we use tool calling instead of generateObject?

We use `generateObject` to extract a value and tool calling to do something — that split settles most design debates. `generateObject` has no side effects: it turns unstructured input into one typed object and stops. Tool calling — `generateText` or `streamText` with a `tools` map — lets the model decide to call functions, such as searching a database or hitting an API, possibly several times in a loop, before it answers.

`experimental_output`, still flagged experimental in the SDK, is the bridge between the two. Used with `generateText` and a `tools` map, the model runs its tool-calling loop and then returns a final answer validated against a schema, so we get the actions and a typed result from one call instead of stitching a second `generateObject` onto the end.

How do we handle a model that returns invalid JSON?

When generation fails schema validation, the AI SDK throws `NoObjectGeneratedError`, and catching it explicitly is the difference between a graceful fallback and a 500. The error carries the raw text the model produced, plus token usage and the response, so we can log exactly what came back and what it cost.

Three habits cut our failure rate before the catch block ever runs. First, `.describe()` on every non-obvious field, because the description is sent to the model — `z.string().describe('ISO 8601 date')` beats hoping it guesses the format. Second, `.nullable()` over `.optional()` for fields the model might not know, because many models emit `null` more reliably than they omit a key. Third, `experimental_repairText`, a callback that receives the raw text and can strip a code fence or a trailing comma before the SDK re-parses. We only reach for a full retry after those three, because a retry doubles latency and cost.

FAQ

What is the difference between generateObject and generateText? `generateText` returns a free-form string; `generateObject` returns a typed object validated against a schema and throws if the output does not match. Use `generateText` for prose and `generateObject` whenever you need machine-readable data.

Does generateObject work with any model? It works with any provider the AI SDK supports, but the mechanism varies — some providers use native structured-output JSON mode, others use tool calling under the hood. You pass the same Zod schema regardless, and models with native structured output are the most reliable.

Can we stream a structured object to the browser? Yes. `streamObject` returns a `partialObjectStream` of deep-partial objects. Render each field as it arrives and treat every field as possibly undefined until the stream completes.

How do we classify text without an object wrapper? Use `output: 'enum'` with an `enum` array of allowed labels. The model must return exactly one of the strings, which is stricter and cheaper than an object with a single enum field.

What throws when the model output is malformed? `NoObjectGeneratedError`. It exposes the raw text, token usage, and response so you can log and fall back. Reduce its frequency with field `.describe()` hints, `.nullable()` over `.optional()`, and an `experimental_repairText` callback.