OpenAI Structured Outputs: Guaranteed JSON Schemas with Function Calling
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.
What You Need
Before you begin, make sure you have the following:
- An OpenAI API key. Set it as the environment variable
OPENAI_API_KEYor pass it directly when instantiating the client. The official SDKs read this variable by default. - An OpenAI SDK installed for your language of choice. The examples in this guide use the Python SDK (
pip install openai) and the Node.js SDK (npm install openai). The guide also shows rawcurlcommands that work with any HTTP client. - A model that supports Structured Outputs. As of this writing, models in the
gpt-5family (e.g.,gpt-5.6,gpt-5.4) and later reasoning models support this feature. The official documentation notes that onlygpt-5.4and later models supporttool_search. - Familiarity with basic JSON schema concepts:
type,properties,required,enum, andadditionalProperties. You do not need to be an expert, but you should understand how to describe an object's shape.
What Are Structured Outputs?
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.
How Function Calling Works
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:
- Make a request to the model with tools (functions) it could call.
- Receive a tool call from the model, which includes the function name and arguments.
- Execute code on the application side using the arguments from the tool call.
- Make a second request to the model with the tool output.
- Receive a final response from the model (or more tool calls).
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.
Key Terminology
- Tool: A piece of functionality you give the model access to. A function is a specific kind of tool defined by a JSON schema.
- Tool call: A response from the model indicating it wants to use a tool. It includes the tool name and arguments.
- Tool call output: The result your application generates after executing the tool. This is fed back to the model in a subsequent request.
- Strict mode: A setting (
strict: true) that forces the model to adhere exactly to the schema, including rejecting extra properties. This is essential for Structured Outputs.
Defining a Function Tool with a JSON Schema

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 apropertiesobject listing each parameter. Each parameter can havetype,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 tofalseto prevent the model from adding extra fields. This is required for strict mode.strict: Set totrueto enable Structured Outputs. Whentrue, the model will not deviate from the schema. The official documentation says that whenstrictistrue, the schema must haveadditionalProperties: false.
Using Pydantic or Zod to Generate Schemas
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.
Making a Request with Structured Outputs
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"])
Using the Chat Completions API
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."
Handling the Tool Call Response
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.nameorname: The name of the function being called.function.argumentsorarguments: 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.
Full Tool Calling Loop
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:
- You must append the model's output (including the tool call) to the input list before making the second request. This preserves the conversation context.
- The tool call output must include the
call_idfrom the original tool call. This lets the model match the output to the correct call. - The
instructionsparameter in the second request is optional but useful for guiding the final response. The official documentation says thatinstructions"gives the model high-level instructions on how it should behave while generating a response, including tone, goals, and examples of correct responses." - For reasoning models, the official documentation notes 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.
Advanced: Namespaces and Tool Search
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:
- "For deferred tools, put detailed guidance in the function description and keep the namespace description concise. The namespace helps the model choose what to load; the function description helps it use the loaded tool correctly."
- "Keep the number of initially available functions small for higher accuracy."
- "Aim for fewer than 20 functions available at the start of a turn at any one time, though this is just a soft suggestion."
Best Practices for Defining Functions

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
instructionsparameter (Responses API) or adeveloperrole 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 singlestateparameter 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_idfrom 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()afterquery_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.
Token Usage Considerations
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:
- Limiting the number of functions loaded up front.
- Shortening descriptions where possible.
- Using tool search so deferred tools are loaded only when needed.
- Using fine-tuning to reduce the number of tokens used for function definitions.
Troubleshooting
The model does not call the function
If the model returns a plain text response instead of a tool call, check the following:
- Is
strictset totrue? Without strict mode, the model may return JSON in text form instead of a proper tool call. - Is the function description clear enough? The model needs to understand when to use the function.
- Is the prompt asking for something the function can provide? If the user asks for a horoscope but the function is named
get_weather, the model may not connect them. - Are you using a model that supports Structured Outputs? Older models may not respect the
strictflag.
The model returns extra fields or wrong types
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.
Token limit exceeded
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.
Reasoning models and tool calls
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.
Going Further
Now that you understand Structured Outputs and function calling, you can explore these next steps:
- Build a prompt in the Playground. The official documentation recommends using the Playground to develop and iterate on prompts. You can test your function schemas there before writing code.
- Explore the full API reference. The text generation guide points to the API reference for all available options, including
reasoning,instructions, andstore. - Learn about tool search. If you have many functions, read the tool search guide to understand how to defer loading and let the model search for relevant tools dynamically.
- Consider fine-tuning. For applications with a large number of functions or difficult tasks, fine-tuning can improve function calling accuracy. The official documentation links to a cookbook for this.
- Manage conversation state. The text generation guide mentions the
previous_response_idparameter for multi-turn conversations. Learn how to use it to maintain context across requests. - Version prompts in code. The official documentation recommends storing production prompts in your application code instead of using reusable prompt objects, which are being deprecated. Use typed function arguments or schemas for dynamic values.
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.
Comments
More Guides
View allOpenAI Batch API: Cut Costs for Bulk Processing
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.
ChatGPT for Coding: Prompt Patterns That Produce Working Code
Learn how to write prompts for ChatGPT that reliably generate working code. Covers core concepts, setup, and specific patterns for different coding tasks.
OpenAI Vision API: Processing Images with GPT Models
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.
Streaming ChatGPT Responses in Web Applications: A Complete Guide
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.
OpenAI Embeddings for Semantic Search: A Practical Guide
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.
Handling OpenAI API Rate Limits and Quota Errors: A Complete Guide
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.