OpenAI Structured Outputs: JSON Schema
Constrain Responses API output with text.format json_schema, parse straight into Pydantic or Zod, and use the same strict schemas for function calling.
On this page5 sections
Structured Outputs make the model’s reply conform to a JSON schema you supply. It is enforcement, not prompting — so you can drop the retry-on-bad-JSON wrapper.
Raw JSON schema
curl https://api.openai.com/v1/responses \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-6-astra",
"input": "Extract: John Smith (john@example.com) wants the Enterprise plan.",
"text": {
"format": {
"type": "json_schema",
"name": "contact",
"strict": true,
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"plan": {"type": "string"}
},
"required": ["name", "email", "plan"],
"additionalProperties": false
}
}
}
}'
The format object needs all four fields: type, a name for the schema, the schema itself, and strict: true. Under strict, every property must be listed in required and additionalProperties must be false.
Python with Pydantic
from pydantic import BaseModel
from openai import OpenAI
class Contact(BaseModel):
name: str
email: str
plan: str
client = OpenAI() # reads OPENAI_API_KEY
response = client.responses.parse(
model="gpt-6-astra",
input="Extract: Jane Doe (jane@co.com) wants Enterprise.",
text_format=Contact,
)
contact = response.output_parsed
print(contact.email) # jane@co.com
responses.parse() builds the schema from the Pydantic model, sends it, and returns a validated Contact on output_parsed. No json.loads, no schema written twice.
JavaScript with Zod
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
const Contact = z.object({
name: z.string(),
email: z.string(),
plan: z.string(),
});
const client = new OpenAI(); // reads OPENAI_API_KEY
const response = await client.responses.parse({
model: "gpt-6-astra",
input: "Extract: Jane Doe (jane@co.com) wants Enterprise.",
text: { format: zodTextFormat(Contact, "contact") },
});
console.log(response.output_parsed.email);
zodTextFormat(schema, name) converts the Zod object into the wire format and gives output_parsed its type.
The same schemas as function tools
tools = [{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"strict": True,
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
"additionalProperties": False,
},
}]
response = client.responses.create(model="gpt-6-astra", input="Weather in Paris?", tools=tools)
for item in response.output:
if item.type == "function_call":
print(item.name, item.arguments) # get_weather {"location":"Paris, France"}
Function tools sit flat on the tool object (type, name, description, parameters, strict) — there is no nested function wrapper in the Responses API. arguments arrives as a JSON string, so parse it, then send the result back as {"type": "function_call_output", "call_id": item.call_id, "output": "..."} in the next input.
Refusals
for item in response.output:
if item.type == "refusal":
print("Model declined:", item.refusal)
A safety refusal is a distinct output item, not malformed JSON — check for it before assuming your parse failed.
Next: your first OpenAI request · reliable JSON from Claude.