1. ai
  2. /building
  3. /structured-outputs

Structured Outputs

Structured outputs turn free-form model text into typed objects your app can validate, store, and render — without fragile regex on chat responses.

Last reviewed: June 2026

Provider APIs for JSON mode and schema enforcement change frequently. Pin SDK versions and verify OpenAI structured outputs and Anthropic docs before production.

The problem

Raw completion:

The sentiment is positive with high confidence.

Your parser breaks when the model adds a preamble, uses "Positive" instead of "positive", or omits a field. Structured outputs constrain the response to a schema you define.

Use structured outputs when:

  • Extracting form fields, entities, or classifications
  • Routing user intent to handlers
  • Generating config or API payloads consumed by code
  • Building eval harnesses with machine-readable scores

Skip structured outputs when:

  • User-facing prose is the product (chat, docs)
  • Schema changes every request (prefer tool calling with dynamic tools)

Approach comparison

ApproachProviderGuarantee level
Zod + generateObject (AI SDK)OpenAI, Anthropic, othersSchema-validated object; SDK retries on parse failure
OpenAI response_format: json_schemaOpenAIConstrained decoding to JSON Schema
Anthropic tool use with single toolAnthropicTool input validated against schema
Prompt "respond with JSON only"AnyUnreliable — use only for prototypes

Prefer schema-enforced paths for production. See OpenAI API and Anthropic API.

Works across providers with one pattern:

import { openai } from "@ai-sdk/openai";
import { generateObject } from "ai";
import { z } from "zod";

const TicketSchema = z.object({
  category: z.enum(["billing", "technical", "other"]),
  priority: z.enum(["low", "medium", "high"]),
  summary: z.string().max(200),
  suggestedActions: z.array(z.string()).max(5),
});

export async function POST(req: Request) {
  const { message } = await req.json();

  const { object } = await generateObject({
    model: openai("gpt-4o-mini"),
    schema: TicketSchema,
    prompt: `Classify this support message:\n\n${message}`,
  });

  return Response.json(object);
}

Install: npm install ai @ai-sdk/openai zod

OpenAI native JSON Schema

Direct API when not using the AI SDK:

const response = await openai.chat.completions.create({
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: userText }],
  response_format: {
    type: "json_schema",
    json_schema: {
      name: "ticket_classification",
      strict: true,
      schema: {
        type: "object",
        properties: {
          category: { type: "string", enum: ["billing", "technical", "other"] },
          priority: { type: "string", enum: ["low", "medium", "high"] },
          summary: { type: "string" },
        },
        required: ["category", "priority", "summary"],
        additionalProperties: false,
      },
    },
  },
});

const parsed = TicketSchema.parse(JSON.parse(response.choices[0].message.content!));

Always validate with Zod after parse — defense in depth.

Anthropic pattern (tool as schema)

Define one tool whose input_schema is your output shape:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 1024,
  tools: [{
    name: "classify_ticket",
    description: "Return structured ticket classification",
    input_schema: {
      type: "object",
      properties: {
        category: { type: "string", enum: ["billing", "technical", "other"] },
        priority: { type: "string", enum: ["low", "medium", "high"] },
        summary: { type: "string" },
      },
      required: ["category", "priority", "summary"],
    },
  }],
  tool_choice: { type: "tool", name: "classify_ticket" },
  messages: [{ role: "user", content: userText }],
});

const toolUse = response.content.find((b) => b.type === "tool_use");
const object = TicketSchema.parse(toolUse?.input);

Model IDs change — verify current IDs in Anthropic docs.

Schema design tips

TipExample
Use enums for closed setsz.enum(["open", "closed"]) not free string
Cap array length.max(5) on suggestedActions
Cap string length.max(200) on summaries
Optional vs requiredOnly mark optional what you truly tolerate missing
Describe fields in .describe()Helps model fill ambiguous fields

Production concerns

ConcernWhat to do
CostUse smaller models (gpt-4o-mini, Haiku) for classification
LatencyStructured output adds minimal overhead vs free text
Failure modesWrap in try/catch; Zod parse errors → fallback or retry once
DriftVersion schemas; log raw responses in staging
SecurityNever eval() parsed JSON; treat as untrusted input

Streaming structured output

For UX that shows partial JSON, use AI SDK streamObject:

import { streamObject } from "ai";

const result = streamObject({
  model: openai("gpt-4o-mini"),
  schema: TicketSchema,
  prompt: userText,
});

return result.toTextStreamResponse();

Client receives incremental partial object updates.