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- Install the SDK and Pydantic with
pip install openai pydantic. - Write a
BaseModelclass for each object in the shape you want back. - Call
client.responses.parsewith your input andtext_formatset to the top-level class. - Make sure the model neither refused nor stopped early before you read the result.
- Read
response.output_parsedand 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;| 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.
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 launches
Model outputs reliably adhere to JSON Schemas you supply.
The Responses API ships
A new API for agents and tools, the one this course uses.
Strict mode with parallel calls, and more keywords
String patterns and formats such as
emailjoin the supported subset.
Source: OpenAI Docs: Changelog (read November 5, 2026)
| 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 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
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_textbefore checking for a refusal or an incomplete status. - Treating a
completedstatus as proof that a parsed object exists and is ready to use. - Leaving a field out of
requiredto 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
allOforifandthenin 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.stepsone step per line. - Change
final_answertostr | Noneand 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_tokenslow enough to cut the reply off and readincomplete_details. - Write the math schema by hand as
json_schema, removeadditionalProperties, 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
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.
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.
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.
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
- OpenAI Docs: Structured model outputs https://developers.openai.com/api/docs/guides/structured-outputs
- OpenAI Docs: Function calling https://developers.openai.com/api/docs/guides/function-calling
- OpenAI Docs: Migrate to the Responses API https://developers.openai.com/api/docs/guides/migrate-to-responses
- OpenAI Docs: Changelog https://developers.openai.com/api/docs/changelog
- X: Michelle Pokrass announces Structured Outputs https://x.com/michpokrass/status/1820881057824305567
- X: OpenAI Developers on strict mode with parallel calls and new schema keywords https://x.com/OpenAIDevs/status/1924915341052019166


