Mindket

OpenAI Structured Outputs: JSON that matches your schema

A black banner titled "OpenAI API" over the line "Responses, tools, structured output and agents".

Structured Outputs makes an OpenAI model reply with JSON that follows the JSON Schema you supply, so no required key goes missing and no enum value is invented. In the Responses API you pass the schema in text.format with strict on, or hand the SDK a Pydantic model or Zod object and read the typed result from output_parsed.

In this lesson

  • Define the shape once in code and let the SDK build the schema from it.
  • Branch on refusal and incomplete status before you trust the parsed object.
  • Tell the model what to return when the input does not fit the shape.
  • Keep the root a plain object and model optional fields as nullable.
  • Reach for a tool call when the model acts, and for a reply format when it answers.
All courses
On this page
  1. Before you start
  2. Get a typed reply with Pydantic
  3. Use Zod, or pass the JSON Schema yourself
  4. Handle refusals and cut-off replies
  5. What a schema can and cannot hold
  6. Michelle Pokrass and OpenAI Developers on schema reliability
  7. Common mistakes
  8. Try it yourself
  9. Next
  10. Check yourself
  11. Frequently asked questions
  12. Sources

Before you start

You need the setup from lesson 1, an API key in OPENAI_API_KEY and the official SDK, plus a schema library. Python uses Pydantic, so run pip install openai pydantic. JavaScript uses Zod, which the samples import next to the SDK, so run npm install openai zod. Pydantic's BaseModel and Zod's z.object both describe a data shape in code, and the SDK turns that shape into JSON Schema for you. JSON Schema is a standard way to describe JSON data: which keys an object has, what type each value is and which keys are required.

Structured Outputs works on OpenAI models from GPT-4o onward, while older models such as gpt-4-turbo fall back to JSON mode. The guide points new projects at gpt-6-astra, the model in every sample here on 5 November 2026. It comes in two forms. Lesson 2 already used one: strict on a function definition, which holds the model's arguments to your schema. This lesson covers the other, text.format, which holds the model's reply to the user to a schema.

The guide draws the line simply. When the model is connecting to your functions, data or tools, use function calling. When you want to shape what the model says back, such as a math tutor whose steps your interface renders one by one, use text.format. A refund flow that looks up an order is a function. A support reply that your interface splits into a summary and next steps is a reply format. Code moving from Chat Completions should note that response_format became text.format in the Responses API.

The guide shows four jobs for it. A math tutor returns its working as ordered steps. An extractor pulls the title, authors, abstract and keywords out of a research paper. A UI generator returns HTML as a recursive tree of elements held to enums. A moderation check sorts input into categories.

Get a typed reply with Pydantic

Ask a math tutor "how can I solve 8x + 7 = -23" with a schema attached, and the docs' example response is plain JSON. It has a steps list of five objects. Each pairs an explanation, such as "Divide both sides by 8 to solve for x.", with an output, such as "x = -30 / 8". It ends with "final_answer": "x = -15 / 4". No prose wraps the JSON, and no key is missing.

The code below produced that shape in the guide. Two Pydantic classes describe it: a Step with two strings, and a MathReasoning that holds a list of steps and a final answer. The client.responses.parse call takes the class as text_format, sends the matching JSON Schema with strict mode on, and parses the reply back into a MathReasoning instance.

Your code then reads response.output_parsed as a typed object, so math_reasoning.final_answer is a string your editor knows about. The guide lists what this saves. You skip validating and retrying malformed replies, refusals become detectable in code, and the prompt no longer has to plead for a format.

The JavaScript version of the same example builds Step and MathReasoning with z.object, using z.array(Step) for the list. It passes zodTextFormat(MathReasoning, "math_reasoning") as text.format and reads the same output_parsed field.

from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Step(BaseModel):
    explanation: str
    output: str

class MathReasoning(BaseModel):
    steps: list[Step]
    final_answer: str

response = client.responses.parse(
    model="gpt-6-astra",
    input=[
        {
            "role": "system",
            "content": "You are a helpful math tutor. Guide the user through the solution step by step.",
        },
        {"role": "user", "content": "how can I solve 8x + 7 = -23"},
    ],
    text_format=MathReasoning,
)

math_reasoning = response.output_parsed
  1. Install the SDK and Pydantic with pip install openai pydantic.
  2. Write a BaseModel class for each object in the shape you want back.
  3. Call client.responses.parse with your input and text_format set to the top-level class.
  4. Make sure the model neither refused nor stopped early before you read the result.
  5. Read response.output_parsed and use its fields like any Python object.

Use Zod, or pass the JSON Schema yourself

In JavaScript the same idea runs through Zod. The guide's opening example defines a CalendarEvent with a name, a date and a list of participants, then passes zodTextFormat(CalendarEvent, "event") as text.format to openai.responses.parse. The second argument names the format. Given "Alice and Bob are going to a science fair on Friday.", the reply parses into that event shape, and response.output_parsed holds the typed object.

Without a helper, you write the schema by hand. Pass text: { format: { type: "json_schema", name: "math_response", schema: {...}, strict: true } } to responses.create and read the JSON string from output_text. The name field labels the format, and schema holds the JSON Schema itself. Every object in the schema needs additionalProperties: false and every property listed in required, the same rules lesson 2 met for strict functions. The model writes keys in the order your schema lists them. The math example puts steps before final_answer, so the working comes out before the answer.

The first request with any new schema has extra latency while the API processes it. Later requests with the same schema skip that cost. A schema that changes on every call pays it every time.

You can also mix the two styles. One of the guide's Python samples writes the format by hand but builds the schema from a Pydantic class with CrateCount.model_json_schema(). That class sets model_config = ConfigDict(extra="forbid"), which makes the generated schema forbid extra keys.

Hand-written schemas carry one more risk: the schema and your types drift apart. The guide strongly recommends the SDK helpers for that reason. If you keep raw schemas, it suggests a CI step that generates the schema from your type definitions, or a rule that flags edits to either one. For the schema itself, name keys clearly, give important keys titles and descriptions, and run evals to find the structure that works.

JSON mode is the older, weaker setting, and the table shows how it compares. It only promises valid JSON, not your schema. It also needs the word "JSON" somewhere in the context, or the API returns an error. Without that instruction the model may emit whitespace until it hits the token limit. Use it only on a model that lacks Structured Outputs, with a validation library and retries.

import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";

const openai = new OpenAI();

const CalendarEvent = z.object({
  name: z.string(),
  date: z.string(),
  participants: z.array(z.string()),
});

const response = await openai.responses.parse({
  model: "gpt-6-astra",
  input: [
    { role: "system", content: "Extract the event information." },
    {
      role: "user",
      content: "Alice and Bob are going to a science fair on Friday.",
    },
  ],
  text: {
    format: zodTextFormat(CalendarEvent, "event"),
  },
});

const event = response.output_parsed;
From the Structured Outputs guide, read on 5 November 2026.
Structured OutputsJSON mode
Valid JSONYesYes
Follows your schemaYes, within the supported subsetNo
Turned on withtext.format type json_schema and strict: truetext.format type json_object
ModelsFrom gpt-4o-mini and gpt-4o-2024-08-06 onOlder GPT-3.5, GPT-4 and GPT-4o models, plus compatible GPT-5 models

Handle refusals and cut-off replies

A reply can fail to match your schema in two ways, and both are ordinary. The model can refuse for safety reasons, or it can run out of room before the JSON closes.

A refusal does not follow your schema, so it arrives as its own content type. Inside the message item, the content part has type set to "refusal" and a refusal field with the model's words. In the guide's sample JSON that text reads "I'm sorry, I cannot assist with that request." The response status still says completed, so code that only checks the status will try to parse a refusal.

The loop below is the end of the guide's refusal example. It skips items that are not messages, prints any refusal, and raises an error when a part has nothing parsed. In an app you would show the refusal in your interface or route it to a fallback instead of printing it.

A cut-off reply looks different. When you set max_output_tokens and the model reaches it, response.status is "incomplete" and response.incomplete_details.reason is "max_output_tokens". The JSON stops partway and will not parse, so check for that status before anything else, then raise the limit or shorten the task. The guide's fuller edge-case sample checks in that order: the incomplete status first, then the first message item, then whether its first part is a refusal or text.

Streaming works with a schema as well. In Python, client.responses.stream takes the same text_format, and the loop checks response.refusal.delta, response.output_text.delta, response.error and response.completed. When the stream ends, stream.get_final_response() returns the full response. In JavaScript, openai.responses.stream takes a zodTextFormat, listens with .on("response.output_text.delta", ...) and its refusal twin, and await stream.finalResponse() returns the result. The guide recommends letting the SDK handle streamed Structured Outputs rather than parsing the pieces yourself.

User input needs one more guard. The model always tries to fit the schema, so an input that has nothing to do with the task can come back as invented values in a valid shape. Say in your instructions what to return in that case, such as empty fields or a fixed sentence. Without that instruction, a valid-looking record built from nothing is hard to spot downstream.

for output in response.output:
    if output.type != "message":
        continue

    for item in output.content:
        if item.type == "refusal":
            # If the model refuses to respond, you will get a refusal message
            print(item.refusal)
            continue

        if not item.parsed:
            raise Exception("Could not parse response")

        print(item.parsed)

What a schema can and cannot hold

Structured Outputs supports a subset of JSON Schema. The types are string, number, boolean, integer, object, array, enum and anyOf. Strings can carry a pattern, a regular expression the value must match, and a format: date-time, time, date, duration, email, hostname, ipv4, ipv6 or uuid. Numbers can carry minimum, maximum, their exclusive forms and multipleOf. Arrays can carry minItems and maxItems.

Some keywords are not supported yet: allOf, not, if, then, else, dependentRequired and dependentSchemas. Send one with strict: true and the API returns an error instead of a quiet best effort. Fine-tuned models also skip the string, number and array constraints above, plus patternProperties on objects.

The root of the schema must be an object, and it must not be anyOf. This one catches Zod users. A discriminated union, such as a success shape and an error shape keyed on status, compiles to anyOf at the top level, and the guide shows that it fails. Wrap the union in an object field instead. Inside the schema, anyOf works as long as each branch is itself a valid schema under the same subset.

Every field must be in required, so the model always returns a value for it. To make a field optional, keep it required and allow null in its type, which in Pydantic is a field typed str | None. The guide's moderation example shows the result. Its model has category: Category | None and explanation_if_violating: str | None. For "How do I prepare for a job interview?" the docs' example response sets is_violating to false and both of those fields to null. Every object also needs additionalProperties: false. Definitions under $defs work, so a shape used twice can be written once and referenced. The guide's sample defines step there and points the steps array at it with "$ref": "#/$defs/step". Recursive schemas work too, with # pointing back to the root.

Size has hard limits, set out in the table. They rarely bind on a schema written by hand. They matter when you generate a schema from a large catalog, such as an enum with every product name, so check the counts before the first request does it for you.

Structured Outputs in the OpenAI changelog
  1. Structured Outputs launches

    Model outputs reliably adhere to JSON Schemas you supply.

  2. The Responses API ships

    A new API for agents and tools, the one this course uses.

  3. Strict mode with parallel calls, and more keywords

    String patterns and formats such as email join the supported subset.

Source: OpenAI Docs: Changelog (read November 5, 2026)

From the supported schemas part of the Structured Outputs guide, read on 5 November 2026.
LimitMaximum
Object properties in the whole schema5,000
Levels of nesting10
Total characters of property names, definition names, enum values and const values120,000
Enum values across all enum properties1,000
Characters across one string enum with more than 250 values15,000

Michelle Pokrass and OpenAI Developers on schema reliability

Michelle Pokrass of OpenAI announced Structured Outputs on X on 6 August 2024, the day it launched. Model outputs would now follow your exact JSON schemas, she wrote, and she gave OpenAI's own figures: 100% schema reliability with the feature on, up from 86% on an earlier GPT-4o model. Those numbers come from OpenAI's tests of GPT-4o in August 2024, not of today's models. The point still holds, though. With strict mode on, the schema is enforced, not requested.

OpenAI Developers posted on 20 May 2025 that strict mode now worked with parallel function calls. The same post listed many more supported schema keywords: regex and formats such as email for strings, ranges for numbers, size limits for arrays. They are the pattern, format, minimum and minItems keywords in the schema rules here. Put those limits in the schema. Only fine-tuned models skip them, and only there does your code need to check again.

schema reliability is now 100% with this feature enabled, up from 86% on our past gpt-4o model

Michelle Pokrass, Works at OpenAI · X (read November 5, 2026)
Michelle Pokrass (@michpokrass) on X, 6 August 2024: Excited to announce Structured Outputs, our newest feature in the API. Model outputs will now reliably follow your exact JSON schemas, matching the parameters and types accurately. Schema reliability is now 100% with this feature enabled, up from 86% on our past gpt-4o model.
Michelle Pokrass on X, 6 August 2024
OpenAI Developers (@OpenAIDevs) on X, 20 May 2025: We have made some improvements to Structured Outputs. Parallel function calling now works with strict mode, so calls reliably adhere to schema. Many more keywords are now supported: string lengths and formats via regex or formats like email, min and max ranges for numbers, and min and max elements in arrays.
OpenAI Developers on X, 20 May 2025

Screenshots of X's own embeds of the posts, taken on 5 November 2026, with the time in UTC. Select one to open the post on X. X cuts the OpenAI Developers post short with Show more.

Common mistakes

  • Parsing output_text before checking for a refusal or an incomplete status.
  • Treating a completed status as proof that a parsed object exists and is ready to use.
  • Leaving a field out of required to make it optional, so the request fails.
  • Using a Zod discriminated union as the root of the schema.
  • Changing the schema on every request and paying the first-request latency each time.
  • Keeping a hand-written schema and a separate type that slowly stop matching.
  • Choosing JSON mode on a model that supports Structured Outputs.
  • Reaching for allOf or if and then in a strict schema, which the subset leaves out.
  • Sending user input with no instruction for what to return when it does not fit.

Try it yourself

  • Run the math tutor sample, then print math_reasoning.steps one step per line.
  • Change final_answer to str | None and ask a question with no solution. What comes back?
  • Send the Zod calendar example a sentence with no event in it, then add an instruction for that case.
  • Set max_output_tokens low enough to cut the reply off and read incomplete_details.
  • Write the math schema by hand as json_schema, remove additionalProperties, and read the error.
  • Build a Zod discriminated union, pass it as the root, and see the request fail.
  • Run the guide's moderation example with a harmless question and print category.

Next

Lesson 4 moves up a level to the OpenAI Agents SDK. An agent there can take an output_type, a Pydantic model or Zod schema, and the SDK uses the same Structured Outputs machinery to make its final answer match.

The SDK also runs the tool-call loop from lesson 2 for you, hands work between agents, checks input with guardrails and records every step in a trace. Each of those builds on the calls this course has made by hand so far. The examples there come in Python and, where the docs show it, TypeScript.

Check yourself

  1. After client.responses.parse with a Pydantic class, where is the typed object?

    Show answer

    A. response.output_parsed The parse helper puts the instance of your class in output_parsed.

  2. Your schema needs an optional field. How do you write it for Structured Outputs?

    Show answer

    C. Keep it in required and allow null in its type Every field must be required, so an optional one stays in required with null as an allowed type.

  3. The model refuses for safety reasons. How does that show up?

    Show answer

    B. As a content part with type refusal A refusal arrives as its own content type inside the message, while the status can still read completed.

  4. You turn on JSON mode with json_object. What must the context contain?

    Show answer

    D. The word JSON Without the word JSON somewhere in the context, the API returns an error for JSON mode.

Frequently asked questions

Can a Structured Outputs reply still be wrong?

Yes. The shape is held to the schema, but the values inside it can still contain mistakes. The guide suggests clearer instructions, examples in the system instructions, or splitting the task into simpler subtasks when you see them. Keep checking the values your app depends on, such as totals or dates, as you would with any model output.

Can I change the schema in the middle of a conversation?

Yes, on supported GPT-6 and later models in standard, single-agent mode. Append a configuration_update item with the new text.format to input before the next user message, and keep the original top-level format on each request so prompt caching still works. Each update replaces the whole format, and {"type": "text"} switches back to plain text. Updates accept only json_schema and text.

Which APIs support Structured Outputs besides Responses?

Chat Completions, Assistants, Fine-tuning and Batch all support it, and JSON mode too. In Chat Completions the schema goes in response_format with type json_schema, which needs gpt-4o-mini, gpt-4o-2024-08-06 or a later snapshot.

Is JSON mode switched on when I use function calling?

Yes. The guide notes that JSON mode is always on with function calling, so arguments are valid JSON. Matching your schema exactly is a separate promise that needs strict on each function, as the function calling guide explains. Strict functions follow the same schema rules as this lesson.

Sources

  1. OpenAI Docs: Structured model outputs (read November 5, 2026)https://developers.openai.com/api/docs/guides/structured-outputs
  2. OpenAI Docs: Function calling (read November 5, 2026)https://developers.openai.com/api/docs/guides/function-calling
  3. OpenAI Docs: Migrate to the Responses API (read November 5, 2026)https://developers.openai.com/api/docs/guides/migrate-to-responses
  4. OpenAI Docs: Changelog (read November 5, 2026)https://developers.openai.com/api/docs/changelog
  5. X: Michelle Pokrass announces Structured Outputs (read November 5, 2026)https://x.com/michpokrass/status/1820881057824305567
  6. X: OpenAI Developers on strict mode with parallel calls and new schema keywords (read November 5, 2026)https://x.com/OpenAIDevs/status/1924915341052019166

Share this lesson

See agents that return clean data

Browse AI agents other developers list on Mindket, read how each one shapes its output, then list one of your own.

Browse AI agents

This lesson as Markdown · OpenAI API course: Responses, tools, structured output and agents · All courses