§15.07

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.

published 06 Sept 2026 checked against docs 06 Sept 2026 4 min in Building with the API Markdown

On this page5 sections
  1. Raw JSON schema
  2. Python with Pydantic
  3. JavaScript with Zod
  4. The same schemas as function tools
  5. Refusals

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.

← All Building with the API plates · Search all guides

↑↓ move↵ openalt+↵ copy first command

Keyboard

⌘/ctrl+K or /
Search all guides
alt+↵
In search: copy the guide's first command
j / k
Move through a list of guides
c
On a guide: copy its first command
t
Toggle light / dark
?
This list