VynarisEarly beta
Get my API key

OpenAI Responses API Structured Outputs: Working Code

Use text.format with type json_schema and strict true in the Responses API. Working code, strict mode schema rules, common errors and token cost.

TL;DR: In the Responses API, structured outputs move from response_format to text.format. Send text: {format: {type: "json_schema", name, schema, strict: true}}, or call client.responses.parse(text_format=YourModel) in Python. Strict mode rejects schemas with optional fields, missing additionalProperties: false, or a root anyOf, and the JSON keys you ask for are billed as output tokens.

To get OpenAI Responses API structured outputs, put a JSON Schema under text.format with type: "json_schema" and strict: true; the model's reply is then constrained to that schema. Structured outputs in the Responses API work on gpt-4o-mini, gpt-4o-2024-08-06 and every later model, and OpenAI's docs point new projects at gpt-6-astra (OpenAI Structured Outputs guide). The Python and JavaScript SDKs accept Pydantic and Zod models and convert them for you. The steps below give a working request, the edge cases you must handle, the schema features strict mode refuses, and what the feature does to your bill. Prices verified 2026-11-05.

What you need

How do I use structured outputs with the Responses API?

Follow these steps. Each one is a single change you can test on its own.

  1. Define the schema as a Pydantic model. Every field is required by default, which is what strict mode wants.
from openai import OpenAI
from pydantic import BaseModel

client = OpenAI()

class Invoice(BaseModel):
    vendor: str
    invoice_date: str
    total_usd: float
    line_items: list[str]
  1. Call `responses.parse` with `text_format`. The SDK builds the JSON Schema, sets strict: true and parses the reply.
response = client.responses.parse(
    model="gpt-6-luna",
    input=[
        {"role": "system", "content": "Extract the invoice fields."},
        {"role": "user", "content": "ACME Corp, 2026-10-30: 3 widgets, 1 bracket. Total $412.50"},
    ],
    text_format=Invoice,
    max_output_tokens=1024,
)
invoice = response.output_parsed  # an Invoice instance
print(invoice.total_usd)
  1. Or send raw JSON Schema with `responses.create`. Use this from other languages, from curl, or when the schema lives in a config file.
schema = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string"},
        "invoice_date": {"type": "string", "format": "date"},
        "total_usd": {"type": "number", "minimum": 0},
        "po_number": {"type": ["string", "null"]},
    },
    "required": ["vendor", "invoice_date", "total_usd", "po_number"],
    "additionalProperties": False,
}

response = client.responses.create(
    model="gpt-6-luna",
    input="ACME Corp, 2026-10-30, total $412.50, no PO.",
    text={"format": {"type": "json_schema", "name": "invoice", "schema": schema, "strict": True}},
    max_output_tokens=1024,
)
print(response.output_text)

Note po_number: an optional field is written as a required field whose type allows null.

  1. Check for incomplete output and refusals before parsing. A response cut off by max_output_tokens is not valid JSON, and a safety refusal comes back as a refusal content item instead of output_text.
import json

if response.status == "incomplete" and response.incomplete_details.reason == "max_output_tokens":
    raise RuntimeError("Output truncated: raise max_output_tokens or shrink the schema")

message = next(item for item in response.output if item.type == "message")
part = message.content[0]
if part.type == "refusal":
    print("Refused:", part.refusal)
else:
    data = json.loads(part.text)
  1. Stream if you need it. Structured outputs stream like any other Responses call; the JSON arrives in schema key order, because the model writes keys in the order your schema lists them.

The request flow, end to end:

The Responses API structured outputs flow from schema to parsed object with two failure checks
The first request with a new schema is slower while OpenAI processes it.

How do I verify it worked?

Print response.output_parsed (Python parse) or run json.loads(response.output_text) and validate it against the same Pydantic model with Invoice.model_validate_json(response.output_text). If validation passes on a few hundred real inputs and you see no refusal items, the integration is done. Expect the first call with a new schema to be slower: OpenAI's guide says the API processes the schema on first use, and later requests with the same schema have no extra latency.

How is text.format different from response_format in Chat Completions?

text.format is the same feature with a flatter shape. The migration guide lists it as one of the API-shape changes.

Chat CompletionsResponses API
Parameterresponse_formattext.format
Schema nesting{"type": "json_schema", "json_schema": {"name", "strict", "schema"}}{"type": "json_schema", "name", "strict", "schema"}
JSON mode{"type": "json_object"}text.format: {"type": "json_object"}
Python helperchat.completions.parse(response_format=Model)responses.parse(text_format=Model)
Read resultchoices[0].message.contentoutput_text or output_parsed
Truncation signalfinish_reason == "length"status == "incomplete", reason max_output_tokens

The common migration bug: copying the Chat Completions object as-is, so name and schema stay wrapped inside a json_schema key. Lift them one level up.

Use text.format when the model answers your user in a fixed shape. Use function calling with strict: true when the model should call your code. For functions, the Responses API tries to normalize a schema to strict when you omit strict and falls back to strict: false if it can't; Chat Completions stays non-strict by default (function calling guide).

What schema features does strict mode not support?

Strict mode supports a subset of JSON Schema. Types allowed: string, number, boolean, integer, object, array, enum and anyOf. These rules cause most rejected requests:

RuleWhat breaks itFix
Every field in requiredPydantic Optional[str] = None with a default, or a hand-written optional keyList the key in required, type ["string", "null"]
additionalProperties: false on every objectNested objects without itAdd it to each object, including array items
Root is an object, not anyOfZod discriminatedUnion at the top levelWrap the union in a property of a root object
No composition keywordsallOf, not, if/then/else, dependentRequired, dependentSchemasFlatten into one object or use anyOf inside a property
Size limitsMore than 5,000 object properties or 10 nesting levelsSplit the task into two calls
String budgetOver 120,000 characters across property names, definition names, enum and const valuesShorten names, move long enums to the prompt
Enum limitsOver 1,000 enum values in total, or over 15,000 characters in one enum with 250+ valuesUse a string with a pattern, validate after
A nullable required field replaces an optional key in strict mode
Strict mode needs every key in required.

Supported constraints include pattern and format on strings (date-time, time, date, duration, email, hostname, ipv4, ipv6, uuid), minimum, maximum, exclusiveMinimum, exclusiveMaximum and multipleOf on numbers, and minItems/maxItems on arrays. $defs and recursive schemas ("$ref": "#") are supported. Fine-tuned models drop the type-specific keywords: no minLength, maxLength, pattern, format, minimum, maximum, multipleOf, patternProperties, minItems or maxItems.

Common errors and fixes

When you send strict: true with an unsupported schema, the request fails before generation with an error that lists the missing constraints. The causes you will see most:

Do structured outputs cost more tokens?

There is no surcharge for structured outputs: you pay the model's normal input and output rates. The cost comes from tokens. Every key name, quote and brace in the JSON is an output token, and output costs 5x input on the GPT-6 models. OpenAI documents that function definitions are injected into the system message and billed as input tokens; for text.format, measure your own schema's overhead by comparing response.usage.input_tokens with and without it:

for fmt in (None, {"format": {"type": "json_schema", "name": "invoice", "schema": schema, "strict": True}}):
    kwargs = {"text": fmt} if fmt else {}
    r = client.responses.create(model="gpt-6-luna", input="ACME Corp, total $412.50", **kwargs)
    print(bool(fmt), r.usage.input_tokens, r.usage.output_tokens)

A worked cost model for 1,000 invoice extractions. The token counts are assumptions: edit them to your measured usage numbers. Prices are OpenAI's standard short-context rates, verified 2026-11-05 (OpenAI pricing).

AssumptionValue
Input tokens per call (prompt + document + schema)400
Output tokens per call (the JSON)150
Calls1,000
ModelInput $/1MOutput $/1MCost per 1,000 callsBatch API
gpt-6-luna$0.10$0.50$0.115$0.0575
gpt-6.1-sol$2.00$10.00$2.30$1.15
gpt-6-astra$10.00$50.00$11.50$5.75

Derivation for gpt-6-luna: 1,000 x (400 x $0.10 + 150 x $0.50) / 1,000,000 = $0.115. Output is 65% of that bill ($0.075 of $0.115), so verbose key names cost more than a long schema. Two levers follow: short, clear key names, and dropping fields you never read. Run your own token counts through the LLM cost calculator to compare models at your volume.

The tradeoff: strict mode removes the retry loop you would build around JSON mode, but it does not make the values correct. A model can return a perfectly valid total_usd that is wrong. Keep an eval set for field accuracy, and don't pay for gpt-6-astra on extraction that gpt-6-luna gets right; at the prices above that is a 100x difference per call.

When some documents need the large model and most don't, routing is the next problem. Vynaris is an OpenAI-compatible router that sends each call to the cheapest model that handles it, so the same text.format request can run on a small model by default and escalate only when needed.

For more on how these costs add up, see our OpenAI API pricing breakdown, the gpt-6-astra cost per 1,000 calls vs gpt-5.6-sol, why cache reads dominate agent costs and the model list.

FAQ

What is the difference between structured outputs and JSON mode?

JSON mode (type: "json_object") guarantees valid JSON but not your schema. Structured outputs (type: "json_schema" with strict: true) guarantee both. OpenAI recommends structured outputs whenever the model supports them.

Does the Responses API support Pydantic?

Yes. client.responses.parse(text_format=MyModel) converts the Pydantic model to a strict JSON Schema and returns an instance in response.output_parsed. In JavaScript, use zodTextFormat from openai/helpers/zod.

Which models support structured outputs?

gpt-4o-mini, gpt-4o-2024-08-06 and every later model, including the GPT-5 and GPT-6 families. Older models like gpt-4-turbo support only JSON mode.

How do I make a field optional in strict mode?

Keep it in required and allow null: "type": ["string", "null"]. In Pydantic, declare po_number: str | None without a default.

Why is my first structured outputs request slow?

The API processes each new schema on first use. Later requests with the same schema have no added latency, so keep schemas stable instead of generating a new one per request.

Can I use structured outputs with the Batch API?

Yes. OpenAI lists structured outputs and JSON mode as supported in the Responses, Chat Completions, Assistants, Fine-tuning and Batch APIs. Batch pricing is half the standard rate, which halves the extraction bill in the table above.