# OpenAI Structured Outputs: JSON that matches your schema

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

Mindket editors · Published 2026-11-05 · Updated 2026-11-05 · https://mindket.com/learn/openai-api/openai-structured-outputs

## In short

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

## 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](https://mindket.com/learn/openai-api/openai-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.

````python
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.

````javascript
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;
````

|  | Structured Outputs | JSON mode |
| --- | --- | --- |
| Valid JSON | Yes | Yes |
| Follows your schema | Yes, within the supported subset | No |
| Turned on with | `text.format` type `json_schema` and `strict: true` | `text.format` type `json_object` |
| Models | From `gpt-4o-mini` and `gpt-4o-2024-08-06` on | Older 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.

````python
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. **August 6, 2024** Structured Outputs launches. Model outputs reliably adhere to JSON Schemas you supply.
2. **March 11, 2025** The Responses API ships. A new API for agents and tools, the one this course uses.
3. **May 20, 2025** Strict mode with parallel calls, and more keywords. String patterns and formats such as `email` join the supported subset.

Source: [OpenAI Docs: Changelog](https://developers.openai.com/api/docs/changelog) (read 2026-11-05)

| Limit | Maximum |
| --- | --- |
| Object properties in the whole schema | 5,000 |
| Levels of nesting | 10 |
| Total characters of property names, definition names, enum values and const values | 120,000 |
| Enum values across all enum properties | 1,000 |
| Characters across one string enum with more than 250 values | 15,000 |

## Michelle Pokrass and OpenAI Developers on schema reliability

Michelle Pokrass of OpenAI [announced Structured Outputs on X](https://x.com/michpokrass/status/1820881057824305567) 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](https://x.com/OpenAIDevs/status/1924915341052019166) 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](https://x.com/michpokrass/status/1820881057824305567) (read 2026-11-05)

[![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.](https://mindket.com/blog/shots/x-michpokrass-structured-outputs.webp?v=b288c5c35c34)](https://x.com/michpokrass/status/1820881057824305567)

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.](https://mindket.com/blog/shots/x-openaidevs-parallel-strict.webp?v=88338d1baf8f)](https://x.com/OpenAIDevs/status/1924915341052019166)

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](https://mindket.com/learn/openai-api/openai-agents-sdk) 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?

   A. response.output_parsed
   B. response.output_text
   C. response.text.format
   D. response.parsed_output

   Answer: A. 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?

   A. Leave it out of required
   B. Give it a default value
   C. Keep it in required and allow null in its type
   D. Set additionalProperties to true

   Answer: C. 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?

   A. As a 400 error
   B. As a content part with type refusal
   C. As a failed status with no output
   D. As a function_call item

   Answer: B. 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?

   A. A JSON Schema
   B. strict set to true
   C. A Pydantic model
   D. The word JSON

   Answer: D. 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](https://developers.openai.com/api/docs/guides/function-calling) explains. Strict functions follow the same schema rules as this lesson.

## Sources

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

[Browse AI agents](https://mindket.com/agents)
