Structured Outputs vs function calling
Structured outputs vs function calling OpenAI: decide when to use schemas, tool calls or both for safer app workflows.
Faisal Karkoh · 6 Nov 2026 · 11 min read
If you are comparing Structured Outputs vs function calling in OpenAI, the decision is not mainly about whether you want JSON. It is about where control passes from the model to your application, which is the same boundary to define before you add AI features to an existing SaaS.
Use Structured Outputs when the model’s final reply must match a schema. Use function calling when the model needs your application to fetch data or perform an action. Use both when a tool result must be turned into a stable UI or API payload.
Evidence note: this article is based on OpenAI documentation and product pages accessed on 2026-11-06. It is researched analysis, not a field test, benchmark or production incident report.
The decision in one table
The practical rule is simple: choose by control flow, not by whether the desired output looks like JSON. A schema can describe a final answer, tool arguments, or both, but those are different application boundaries.
| Job to be done | Choose | Why | Application responsibility | Main failure case |
| Extract typed data from supplied text | Structured Outputs | The answer itself is the artefact your app needs to render, store or pass on. | Validate the object against the source text and handle refusal or incomplete output. | The object is schema-valid but semantically wrong. |
| Render a typed UI card from known context | Structured Outputs | The model can return a predictable payload such as label, summary and next action. | Validate values before display and apply UI fallbacks. | Missing nuance, unsuitable input, or an incomplete response. |
| Ask the app to fetch current or private data | Function calling | The model should request a tool call instead of inventing live state. | Validate arguments, authenticate, authorise, execute, retry and log. | Wrong tool, bad arguments, denied access or timeout. |
| Create, update or delete a record | Function calling | Side effects belong in application code, not in the model’s final text. | Enforce permissions, idempotency, audit logging and human approval where needed. | Unsafe action, duplicate action or unauthorised request. |
| Fetch data, then return a typed UI or API response | Both | Function calling handles the app capability; Structured Outputs handles the final contract. | Validate tool arguments, tool output and final payload separately. | A correct lookup followed by a misleading final object, or a loop that does not stop. |
| Force syntactically valid JSON for a legacy parser | JSON mode or simpler JSON path | If you do not need schema adherence, Structured Outputs may be more than the workflow needs. | Parse JSON and validate fields yourself. | Valid JSON that does not match your expected shape. |
OpenAI’s Structured Outputs guide makes this boundary explicit: use function calling when connecting the model to tools, functions or data in your system, and use a structured response format when the model’s response itself needs a schema, according to the OpenAI Structured Outputs guide.
What Structured Outputs actually gives you
Structured Outputs constrain the model’s final reply to a JSON Schema shape through the response-format path, such as response_format or text.format depending on the API surface. OpenAI’s launch post also explains that Structured Outputs are available in two forms: function calling and a JSON Schema response format for cases where the model is responding to the user in a structured way, as described in OpenAI’s Structured Outputs announcement.
OpenAI’s docs distinguish Structured Outputs from JSON mode by schema adherence, which is the key contract discussed in this section.
That is different from JSON mode. OpenAI’s documentation states that JSON mode ensures valid JSON, while Structured Outputs are intended to make the model adhere to the supplied schema. In app terms, JSON mode may protect a parser; Structured Outputs protect a contract.
Use the support dashboard example. A customer writes:
Can you check order A123 for customer C456? If it has not shipped, change the delivery address to the one below.
If your app only needs to classify or summarise that message, Structured Outputs are enough. A final reply schema might include priority, category, summary, missing_fields and next_action_label. If the address is absent, the object can return category='order_support' and missing_fields=['new_address']. For the wider workflow around triage, escalation and handoff, see the companion guide on how to ship an AI customer support agent that works.
The important limit is that schema-valid does not mean true. OpenAI’s guide notes that Structured Outputs can still contain mistakes, refusals can occur, responses can be incomplete when limits are reached, and unsuitable user input still needs handling. Your application should still compare the extracted values against the source message, reject impossible combinations and decide what to do when the model cannot safely produce the object.
Structured Outputs with response_format: {type: 'json_schema'} are documented for gpt-4o-mini, gpt-4o-mini-2024-07-18, gpt-4o-2024-08-06 and later model snapshots in the OpenAI guide accessed on 2026-11-06. Treat model support as a moving implementation detail and check the current documentation before shipping.
What function calling actually gives you
Function calling is not the model executing your code. It is a model-generated request for your application to call a named tool with structured arguments.
The function-calling flow shows why the model requests a tool call while the application still owns execution.
OpenAI’s function calling documentation describes a multi-step loop: send the available tools, receive a tool call, execute application code, return the tool output, then receive a final response or further tool calls. If you are exposing private company systems rather than public lookup tools, the same execution boundary appears in ChatGPT Actions for internal APIs.
In the support example, the model should not invent whether order A123 has shipped. The app can expose a tool such as lookup_order_status(customer_id, order_id). The model may propose a call with customer_id='C456' and order_id='A123', but application code must validate that the customer can access that order, execute the lookup, handle timeouts and return the tool output.
If the order has not shipped and the user supplied a new address, a second tool such as request_address_change(customer_id, order_id, new_address) may be appropriate. That does not move authority into the model. The application still owns authentication, authorisation, side-effect approval, retries, timeouts, idempotency and audit logging.
Tool selection controls matter when the workflow has risk. OpenAI documents tool_choice controls including automatic choice, required tool use, a forced function, allowed tools and none. It also states that models may choose multiple function calls in one turn; setting parallel_tool_calls:false constrains the turn to zero or one tool call. Use that when your app needs one controlled operation at a time.
There is also a cost and latency shape to this choice. OpenAI’s function-calling guide says callable function definitions count against the model context limit and are billed as input tokens. It also suggests aiming for fewer than 20 functions available at the start of a turn as a soft suggestion, not a hard limit. Exact currency cost depends on the selected model and workload, so this comparison does not include a pricing table.
Where strict mode overlaps
The confusing part is that strict function calling uses Structured Outputs under the hood for tool arguments. That overlap does not change the control-flow boundary: function calling still asks your app to do something; Structured Outputs still constrain an object.
| Strict schema point | Practical effect |
additionalProperties:false on objects | Do not expect undeclared argument fields to pass through as a flexible extension point. |
| All properties listed as required | Design schemas where every field has an explicit value. |
| Nullable optional fields | Represent optional values by including null in the type rather than omitting the field. |
| Strict mode under the hood | Tool arguments can benefit from schema adherence, but execution is still application-owned. |
OpenAI’s current function-calling documentation also describes a dated API-surface difference. If strict is omitted, Responses API requests attempt to normalise compatible schemas into strict mode and can fall back to non-strict; Chat Completions requests remain non-strict by default. That statement is from the OpenAI documentation accessed on 2026-11-06, so verify it against the current API docs during implementation.
One caveat matters for teams using fine-tuned models. OpenAI’s function-calling guide states that if a fine-tuned model calls multiple functions in one turn, strict mode is currently disabled for those calls. If that would break your safety model, constrain the workflow to one call per turn and test that path deliberately.
Schema limits that should affect your design
Structured Outputs support a subset of JSON Schema, not every pattern a backend schema designer might want. For app builders, this should push you towards narrower task-specific schemas rather than one large universal object.
| Documented constraint | Design advice |
Root schema must be an object and must not be a root anyOf | Wrap alternatives in explicit fields instead of making the whole response polymorphic. |
| All fields must be required | Use nullable fields or smaller schemas instead of relying on omitted properties. |
Objects must set additionalProperties:false | Declare the full contract and version it when you need new fields. |
| Up to 5,000 object properties total | Avoid mega-schemas for broad dashboards or all-purpose agents. |
| Up to 10 nesting levels | Keep generated payloads shallow enough for review, logging and UI rendering. |
| Up to 1,000 enum values across enum properties | Do not put a full product catalogue, city list or permissions matrix into an enum. |
| 120,000-character total string length for property names, definition names, enum values and const values | Shorten names and split schemas before the schema itself becomes the workload. |
For the support dashboard, avoid one schema that covers every possible customer-service action. Use a classification schema for message triage, a lookup tool schema for order status, an address-change tool schema for the side effect, and a final UI schema for the result shown to the agent.
That split gives you better failure handling. It also makes the prompt and tool list smaller, which matters because OpenAI documents that function definitions are injected into context and billed as input tokens.
Use both when a tool result needs a stable UI contract
The most production-like pattern is often not either-or. It is a tool call followed by a structured final response, especially when the assistant experience needs something more reliable than free text; the UI decision is covered separately in when a ChatGPT app needs UI instead of text.
| Pattern | Consumer of the artefact | Who owns validation and execution | What can fail | Stop condition |
| Structured Outputs only | Dashboard, queue, database or downstream API that needs a typed object. | The app validates the final object against the source message; there is no tool execution. | Refusal, incomplete response, unsuitable input, or schema-valid but wrong values. | A valid final object is accepted, rejected or sent to human review. |
| Function calling only | Application code that receives a requested tool name and arguments. | The app validates arguments, authorises the operation and executes the tool. | Wrong tool, missing input, bad arguments, denied authorisation, timeout or duplicate side effect. | The tool output is returned to the model, or the app stops with an error or escalation. |
| Function calling plus Structured Outputs | Application code consumes tool calls; UI or API consumes the final typed payload. | The app validates tool arguments, tool output and final structured response separately. | Any tool failure, plus a misleading final status or repeated tool-call loop. | After a bounded number of tool turns, the app receives a valid final object or stops safely. |
In the support example, the combined flow is the cleanest design if the dashboard needs live status and a predictable card. The model first requests lookup_order_status. The app validates and executes the lookup. If a change is allowed and the address is present, the model may request request_address_change, subject to application policy. The final response can then be a structured object with fields such as status_label, can_change_address, customer_message and required_agent_action.
This keeps the ownership boundary clear. The model proposes the next step and formats the final explanation. The application decides whether the step is allowed, performs it, records it and decides when the loop must stop.
Failure cases to handle separately
Do not treat every problem as a generic validation error. The recovery path depends on which boundary failed.
- Refusal: handle it as a final model state, not as malformed JSON. Show a safe fallback or route to review.
- Incomplete structured reply: retry only if your app can do so safely; otherwise return a controlled error or human-review state.
- Schema-valid but wrong values: compare extracted fields against the source text or tool result before writing them to storage.
- Missing user input: return a structured final object that asks for the missing field, such as a delivery address, instead of fabricating it.
- Wrong tool selection: reject the call and either continue with a corrective tool result or stop the loop.
- Bad tool arguments: validate types, identifiers and business rules before execution.
- Denied authorisation: do not ask the model to decide whether the user should have access. Return a denied state from application policy.
- Tool timeout: return a retryable or degraded state. Avoid letting the model loop indefinitely.
- Repeated tool calls: set a maximum number of turns and use
parallel_tool_calls:false when the app requires one operation at a time.
- Side-effect risk: use idempotency keys, audit logs and confirmation gates for updates, payments, fulfilment or irreversible actions.
OpenAI documents streaming support for structured model responses and function call arguments, with SDK helpers recommended. Streaming can improve perceived responsiveness, but it does not remove the need to validate the final object or the complete tool arguments before taking action.
Recommended default for app builders
For most application workflows, start by naming the consumer of the next artefact.
- If the consumer is a UI, database row, queue message or API client, use Structured Outputs for the final reply.
- If the consumer is application code that must fetch live state or perform an operation, use function calling.
- If the workflow needs a lookup or action first and a stable UI or API response afterwards, use both.
- If the integration only needs syntactically valid JSON and not schema adherence, consider JSON mode or a simpler legacy JSON path.
- If your schema is large, deeply nested or enum-heavy, split it into smaller task-specific schemas or narrower tools.
- If multiple tools could run in one turn but the app needs serial control, set explicit loop limits and use
parallel_tool_calls:false.
Before shipping, create fixtures for the support cases you expect: complete request, missing address, shipped order, unauthorised customer, invalid order ID, tool timeout and repeated tool-call attempt. Check both the schema contract and the business rule outcome in CI so prompt, schema and type changes do not silently drift apart.
The next action is to draw the workflow boundary for one real feature: final typed reply, tool call, or tool call followed by a typed final response. Once that boundary is clear, the OpenAI API choice becomes much less ambiguous.