Learn how to use OpenAI Structured Outputs to guarantee JSON schema conformance from GPT models. Covers function tool definitions, strict mode, tool call handling, and best practices for both Chat Completions and Responses APIs.
This guide covers OpenAI's Structured Outputs feature, which lets you guarantee that model responses conform to a JSON schema you define. It is written for developers who want to reliably extract structured data from GPT models, whether for API integrations, data pipelines, or application backends. You will learn how to define function tools with JSON schemas, enforce strict mode, handle tool calls in both the Chat Completions and Responses APIs, and apply best practices for production use.
Before you begin, make sure you have the following:
OPENAI_API_KEY or pass it directly when instantiating the client. The official SDKs read this variable by default.pip install openai) and the Node.js SDK (npm install openai). The guide also shows raw curl commands that work with any HTTP client.gpt-5 family (e.g., gpt-5.6, gpt-5.4) and later reasoning models support this feature. The official documentation notes that only gpt-5.4 and later models support tool_search.type, properties, required, enum, and additionalProperties. You do not need to be an expert, but you should understand how to describe an object's shape.Structured Outputs is an OpenAI feature that forces a model to return JSON that matches a schema you provide. Without it, you rely on prompt engineering to get JSON back, and the model may occasionally omit fields, add extra fields, or return malformed text. With Structured Outputs, the model's response is guaranteed to conform to the schema, as long as the schema itself is valid and the model supports the feature.
According to the official text generation documentation, "In addition to plain text, you can also have the model return structured data in JSON format, this feature is called Structured Outputs." The feature is built on top of function calling (also called tool calling). You define a function with a JSON schema for its parameters, and the model returns a function call with arguments that match that schema. You then extract those arguments as your structured output.
Function calling is the underlying mechanism for Structured Outputs. The official function calling guide describes it as "a multi-step conversation between your application and a model via the OpenAI API." The flow has five high-level steps:
For Structured Outputs, you often stop after step 2: you get the arguments as your structured data, and you do not need to call a real function or continue the loop. But understanding the full flow helps you debug issues and use the feature in more complex scenarios.
strict: true) that forces the model to adhere exactly to the schema, including rejecting extra properties. This is essential for Structured Outputs.
A function tool is defined as an object with specific fields. Here is the structure, taken from the official function calling documentation:
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Units the temperature will be returned in."
}
},
"required": ["location", "units"],
"additionalProperties": false
},
"strict": true
}
Let us walk through each field:
type: Must always be "function". This tells the API that this tool is a function.name: A short, descriptive name for the function. Use snake_case or camelCase consistently. The model uses this name to decide which tool to call.description: A detailed explanation of when and how to use the function. The official documentation says to "Write clear and detailed function names, parameter descriptions, and instructions." This description is injected into the model's context, so it counts toward your token limit.parameters: A JSON schema object that defines the input arguments. It must have "type": "object" and a properties object listing each parameter. Each parameter can have type, description, enum, and nested objects.required: An array of property names that the model must include. If a property is not in this list, the model may omit it.additionalProperties: Set this to false to prevent the model from adding extra fields. This is required for strict mode.strict: Set to true to enable Structured Outputs. When true, the model will not deviate from the schema. The official documentation says that when strict is true, the schema must have additionalProperties: false.If you use Python or Node.js, you can generate function schemas from Pydantic models or Zod schemas instead of writing raw JSON. The official documentation provides examples for both.
Python with Pydantic:
from openai import OpenAI, pydantic_function_tool
from pydantic import BaseModel, Field
client = OpenAI()
class GetWeather(BaseModel):
location: str = Field(..., description="City and country e.g. Bogotá, Colombia")
tools = [pydantic_function_tool(GetWeather)]
completion = client.chat.completions.create(
model="gpt-5.6",
messages=[{"role": "user", "content": "What's the weather like in Paris today?"}],
tools=tools
)
print(completion.choices[0].message.tool_calls)
In this example, GetWeather is a Pydantic BaseModel with one field: location. The pydantic_function_tool helper converts it into the JSON schema format the API expects. Note that the function name defaults to the class name (GetWeather). You can customize it by passing a name argument to pydantic_function_tool.
Node.js with Zod:
import OpenAI from "openai";
import { z } from "zod";
import { zodFunction } from "openai/helpers/zod";
const openai = new OpenAI();
const GetWeatherParameters = z.object({
location: z.string().describe("City and country e.g. Bogotá, Colombia"),
});
const tools = [
zodFunction({ name: "getWeather", parameters: GetWeatherParameters }),
];
const messages = [
{ role: "user", content: "What's the weather like in Paris today?" },
];
const response = await openai.chat.completions.create({
model: "gpt-5.6",
messages,
tools,
store: true,
});
console.log(response.choices[0].message.tool_calls);
Here, zodFunction from the OpenAI helpers converts a Zod schema into a tool definition. The name parameter sets the function name. The .describe() calls on Zod fields become the description in the JSON schema.
The official documentation notes that "Not all pydantic and zod features are supported." For example, complex validators or transforms may not translate correctly. Stick to basic types, nested objects, and enums for reliable results.
Once you have defined your tool, you include it in the tools array of your API request. Here is a complete example using the Responses API in Python, adapted from the official function calling guide:
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_horoscope",
"description": "Get today's horoscope for an astrological sign.",
"parameters": {
"type": "object",
"properties": {
"sign": {
"type": "string",
"description": "An astrological sign like Taurus or Aquarius",
},
},
"required": ["sign"],
"additionalProperties": False,
},
"strict": True,
},
]
input_list = [
{"role": "user", "content": "What is my horoscope? I am an Aquarius."}
]
response = client.responses.create(
model="gpt-5.6",
tools=tools,
input=input_list,
)
print(response.output_text)
In this example, the model receives a user message asking for a horoscope. The tool definition tells the model it can call get_horoscope with a sign parameter. Because strict is true, the model must return a valid JSON object with exactly the sign field and nothing else.
If you run this code, the response will contain a tool call. The response.output array will include an item with type: "function_call". You can extract the arguments like this:
for item in response.output:
if item.type == "function_call":
args = json.loads(item.arguments)
print(args["sign"])
The same pattern works with the older Chat Completions API. Here is the equivalent example from the official documentation:
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "get_horoscope",
"description": "Get today's horoscope for an astrological sign.",
"parameters": {
"type": "object",
"properties": {
"sign": {
"type": "string",
"description": "An astrological sign like Taurus or Aquarius",
},
},
"required": ["sign"],
"additionalProperties": False,
},
"strict": True,
},
},
]
messages = [
{"role": "user", "content": "What is my horoscope? I am an Aquarius."}
]
response = client.chat.completions.create(
model="gpt-5.6",
messages=messages,
tools=tools,
)
tool_call = response.choices[0].message.tool_calls[0]
args = json.loads(tool_call.function.arguments)
print(args["sign"])
Notice that in the Chat Completions API, the tool definition has an extra nesting level: the function object is inside "function": { ... }. In the Responses API, the function fields are at the top level of the tool object. Both work, but the Responses API is the recommended path for new projects. The official text generation documentation says, "If you're building any text generation app, we recommend using the Responses API over the older Chat Completions API."
When the model decides to call a tool, the response includes a tool_calls array (Chat Completions) or an output array with function_call items (Responses). Each tool call has:
id: A unique identifier for this call. You need this when providing tool call output back to the model.type: Either "function" (Chat Completions) or "function_call" (Responses).function.name or name: The name of the function being called.function.arguments or arguments: A JSON string containing the arguments.To extract the structured data, parse the arguments string with json.loads() (Python) or JSON.parse() (JavaScript). The result is a dictionary or object that matches your schema.
If you want the model to use the tool output to generate a final response, you need to complete the loop. Here is a Python example using the Responses API, adapted from the official guide:
from openai import OpenAI
import json
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_horoscope",
"description": "Get today's horoscope for an astrological sign.",
"parameters": {
"type": "object",
"properties": {
"sign": {
"type": "string",
"description": "An astrological sign like Taurus or Aquarius",
},
},
"required": ["sign"],
},
},
]
def get_horoscope(sign):
return f"{sign}: Next Tuesday you will befriend a baby otter."
input_list = [
{"role": "user", "content": "What is my horoscope? I am an Aquarius."}
]
response = client.responses.create(
model="gpt-5.6",
tools=tools,
input=input_list,
)
input_list += response.output
for item in response.output:
if item.type == "function_call":
if item.name == "get_horoscope":
sign = json.loads(item.arguments)["sign"]
horoscope = get_horoscope(sign)
input_list.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": horoscope,
})
response = client.responses.create(
model="gpt-5.6",
instructions="Respond only with a horoscope generated by a tool.",
tools=tools,
input=input_list,
)
print(response.output_text)
Key points:
call_id from the original tool call. This lets the model match the output to the correct call.instructions parameter in the second request is optional but useful for guiding the final response. The official documentation says that instructions "gives the model high-level instructions on how it should behave while generating a response, including tone, goals, and examples of correct responses."For applications with many functions, you can organize them into namespaces. A namespace groups related tools under a domain name. Here is an example from the official documentation:
{
"type": "namespace",
"name": "crm",
"description": "CRM tools for customer lookup and order management.",
"tools": [
{
"type": "function",
"name": "get_customer_profile",
"description": "Fetch a customer profile by customer ID.",
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string"
}
},
"required": ["customer_id"],
"additionalProperties": false
}
},
{
"type": "function",
"name": "list_open_orders",
"description": "List open orders for a customer ID.",
"defer_loading": true,
"parameters": {
"type": "object",
"properties": {
"customer_id": {
"type": "string"
}
},
"required": ["customer_id"],
"additionalProperties": false
}
}
]
}
Namespaces are especially useful with tool_search, a feature that lets the model defer loading rarely used tools until they are needed. Only gpt-5.4 and later models support tool_search. When you set defer_loading: true on a function, it is not loaded into the model's context initially. The model can search for it later if needed.
The official documentation offers these best practices for namespaces and deferred tools:

The official function calling guide provides a comprehensive list of best practices. Here they are, with additional context:
Write clear and detailed function names, parameter descriptions, and instructions. The model uses these to decide when to call a function and what arguments to provide. Vague descriptions lead to incorrect calls.
Explicitly describe the purpose of the function and each parameter (and its format), and what the output represents. For example, if a parameter expects a date, say "Date in YYYY-MM-DD format." If the output is a temperature, say "Temperature in degrees Celsius."
Use the system prompt to describe when (and when not) to use each function. The official documentation says, "Generally, tell the model exactly what to do." You can use the instructions parameter (Responses API) or a developer role message (Chat Completions) for this.
Include examples and edge cases, especially to rectify any recurring failures. However, the documentation also notes, "Adding examples may hurt performance for reasoning models." Test both approaches.
Apply software engineering best practices. Make functions obvious and intuitive. Use enums and object structure to make invalid states unrepresentable. For example, instead of toggle_light(on: bool, off: bool), use a single state parameter with an enum of "on" and "off".
Pass the intern test. The official documentation asks, "Can an intern/human correctly use the function given nothing but what you gave the model? (If not, what questions do they ask you? Add the answers to the prompt.)"
Offload the burden from the model and use code where possible. If you already know a value (like an order_id from a previous step), do not make the model fill it. Instead, hardcode it in your application logic.
Combine functions that are always called in sequence. For example, if you always call mark_location() after query_location(), merge them into one function.
Keep the number of initially available functions small for higher accuracy. Evaluate your performance with different numbers of functions.
Use tool search to defer large or infrequently used parts of your tool surface.
use OpenAI resources. Generate and iterate on function schemas in the Playground. Consider fine-tuning to increase function calling accuracy for large numbers of functions or difficult tasks.
Function definitions count against the model's context limit and are billed as input tokens. The official documentation explains: "Under the hood, functions are injected into the system message in a syntax the model has been trained on. This means callable function definitions count against the model's context limit and are billed as input tokens."
If you hit token limits, the documentation suggests:
If the model returns a plain text response instead of a tool call, check the following:
strict set to true? Without strict mode, the model may return JSON in text form instead of a proper tool call.get_weather, the model may not connect them.strict flag.This should not happen with strict: true and additionalProperties: false. If it does, double-check that your schema is valid JSON Schema. The official documentation says that when strict is true, the model "will not deviate from the schema." If you see deviations, it is likely a bug in your schema definition or an unsupported model version.
If you get a token limit error, reduce the number of functions or shorten their descriptions. Use tool search to defer less common functions. The official documentation also mentions fine-tuning as a way to reduce token usage for function definitions.
For reasoning models like GPT-5 or o4-mini, the official documentation warns that "any reasoning items returned in model responses with tool calls must also be passed back with tool call outputs." If you omit them, the model may lose context or produce incoherent responses. When building the tool calling loop, ensure you append all output items (including reasoning tokens) to the input list.
Now that you understand Structured Outputs and function calling, you can explore these next steps:
reasoning, instructions, and store.previous_response_id parameter for multi-turn conversations. Learn how to use it to maintain context across requests.Structured Outputs give you a reliable way to get structured data from OpenAI models. By combining clear function definitions, strict mode, and proper error handling, you can build robust integrations that depend on consistent JSON output.
Learn how to use the OpenAI Batch API to cut costs by 50% for bulk processing tasks. Covers setup, request formatting, job creation, monitoring, error handling, and troubleshooting.
Learn how to write prompts for ChatGPT that reliably generate working code. Covers core concepts, setup, and specific patterns for different coding tasks.
Learn how to use the OpenAI Vision API to analyze images with GPT models. Covers setup, sending images via URL, Base64, or file ID, controlling detail levels, cost calculation, and troubleshooting.
Learn how to stream ChatGPT responses in web applications using the OpenAI API. This guide covers setup, implementation in JavaScript and Python, event handling, and troubleshooting for real-time text generation.
Learn how to use OpenAI's text embedding models for semantic search, clustering, recommendations, and classification. This guide covers concepts, setup, API usage, dimension reduction, and practical tips from official documentation and community experience.
Learn how to handle OpenAI API rate limits (429) and quota errors. Covers error types, exponential backoff, Python library exceptions, and troubleshooting steps for production applications.
Workflows from the Neura Market marketplace related to this ChatGPT resource