AI Development

OpenAI Agents API + JSON Schema in Practice: Stable, Verifiable, Executable JSON (2026)

About 16 min read

The generic Schema piece already asked why a contract exists. This one lands only on the Agents API: every JSON the model fills must pass a gate before anything runs. Tool parameters, MCP input, and your ledger are three doors — not the sentence “please output JSON.”

Before you read

Door What it locks Who executes
Function parameters order_id, booleans, enums additionalProperties: false
Host check arguments in required_actions No pass, no function
Final card The object shown to a user A second Schema; not text.format
MCP inputSchema on another tool table Subagents may inherit it

Schema is a gate, not a courtesy. A wide object is another door left open.

Stable, verifiable, executable: three procedures, not one switch

On the Agents API the model still only fills JSON. An official function needs a parameters Schema: required, additionalProperties: false, and pattern / enum where it matters. That steadies the fill. Verifiable means you run the same Schema on arguments before you execute. Executable means the host creates side effects only after the gate.

  • Stable: write a tight tool Schema. A wide object invites invented keys.
  • Verifiable: validate the call in required_actions locally. A function_call in history is not enough.
  • Executable: do not run lookup_order if it fails the gate. Write back success: false and a reason. Do not quietly touch the store.

How tools are called: OpenAI Agents API walkthrough. How the four objects pin on IDs: Agents API JSON data structures. The generic split of Schema / Function Calling / Structured Output: Why AI needs JSON Schema. This piece does not redefine those words. It only lands them on an Agents session.

First door: parameters on agent.tools

An official function entry is a name, a description, and a JSON Schema. Write it into agent.tools at session create. Fictional order — no real secrets:

{
  "type": "function",
  "name": "lookup_order",
  "description": "Look up an order by ID. Confirm before any refund.",
  "parameters": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^[A-Z]-\\d{4}$" },
      "include_lines": { "type": "boolean" }
    },
    "required": ["order_id", "include_lines"],
    "additionalProperties": false
  }
}

Only a few levers matter: every field in required; objects close extra keys; IDs use pattern, not a bare string. Functions load eagerly by default. To defer one, set defer_loading: true and put tool_search on the table. The gate itself does not change.

Arguments that fail the gate must not run

When the session stops on agent.session.requires_action, required_actions holds a function_call. This one looks like a lookup and already jumped the fence: the ID fails the pattern, and a refund key appeared:

{
  "type": "function_call",
  "turn_id": "turn_2",
  "call_id": "call_3c91",
  "name": "lookup_order",
  "arguments": {
    "order_id": "1042",
    "include_lines": true,
    "refund": true
  }
}

Same turn_id / call_id pair — a passing shape looks like this. An extra key, a missing required field, a drifted pattern: none of them should execute:

{
  "type": "function_call",
  "turn_id": "turn_2",
  "call_id": "call_3c91",
  "name": "lookup_order",
  "arguments": {
    "order_id": "A-1042",
    "include_lines": true
  }
}

Second door: the host runs the same Schema again

The official Schema improves fills. It does not take your side effects. Validate locally before you run. Record a failure as an object you can review:

{
  "check": "lookup_order",
  "schema": "parameters",
  "pass": ["order_id", "include_lines"],
  "fail": [
    { "path": "/order_id", "reason": "pattern" },
    { "path": "/refund", "reason": "additionalProperties" }
  ],
  "execute": false
}

On failure, send agent.session.input.tool_result with success: false and a readable error. On success, serialize output to a string. Do not open a new session as a “retry.”

{
  "type": "agent.session.input.tool_result",
  "turn_id": "turn_2",
  "call_id": "call_3c91",
  "success": false,
  "error": "order_id failed schema: expected ^[A-Z]-\\d{4}$"
}

The final card is a third door, not Responses text.format

The order card you show a user is not the tool-parameter Schema. An Agents session does not automatically apply Responses text.format.json_schema. Keep three jobs apart:

  • Tool parameters: lock the side-effect door. Lookups and writes go here.
  • Final card: pull an object from items, validate it on a second Schema, then show it.
  • If you must lock the final shape with Structured Outputs: that is the Responses layer. Do not mix it into the same session resource.

Who should own the loop: Agents API vs Responses API. An MCP row is another tool table; its inputSchema still wants additionalProperties: false. Boundary: MCP Tool Schema vs Function Calling. Subagents inherit MCP Schema, not your function.

Five steps to executable JSON

Do not start from “please output JSON.” Walk the doors:

  1. Write the tool Schema first. Tighten pattern, enum, required, additionalProperties: false once.
  2. Load the same Schema into the host validator. Do not keep a second, drifting copy.
  3. On required_actions, validate then execute. Fail the gate → success: false.
  4. Give the final card its own Schema. Validate after you extract from items. Do not treat prose as a card.
  5. Keep ledger fields yourself. Compact drops detail. Amounts and approvals do not live only in the chat.

How a workflow becomes Schema: JSON Schema and agent workflows. This piece only asks that every door on an Agents session is an object.

Review stable output by spreading two JSON trails

Drift is usually a misaligned door, not a dumber model.

  • format parameters next to arguments. Look for extra keys and missing fields.
  • Use JSONPath extract to pull order_id and $.parameters.required.
  • JSON Diff a failing call and a passing one. If the pattern drifted, read the Schema before the model.
  • Review the card Schema and the tool Schema separately. Locking the card but not the parameters is façade work.
  • Do not paste orders, sessions, or intranet MCP URLs into a public formatter. JSONSort runs in the browser locally.

If both trails rebuild against Schema, you collected JSON. Otherwise you collected prose.

Common traps

  • Writing “please output JSON” in instructions only. That is not a gate. The gate is parameters plus the host validator.
  • Listing only some fields in required. Omitted keys may stay empty. The next hop then has holes.
  • Leaving additionalProperties unset or true. Official examples close extra keys. One extra refund and the side effect leans.
  • Copying Responses text.format into an Agents session. Two resources. Validate the final card yourself.
  • Running the function after a failed check. Executable means the gate passed. Write the error first.
  • Sending your function to a subagent. Subagents do not support function tools. Keep the tight Schema on the main agent.

FAQ

Does the Agents API have Structured Outputs?

Tool parameters are tightened with JSON Schema. The object you show a user is not auto-wrapped in Responses text.format. Lock the final shape yourself, or put that hop on Responses.

If the official Schema exists, do I still validate?

Yes. The official gate improves fills. Side effects happen in your process. Run arguments in required_actions through the same Schema; fail with success: false.

Do I need pattern and enum?

Yes. A bare string lets the model fill 1042 instead of A-1042. Amounts and statuses want an enum or an explicit format. Wide types are the usual way over the fence.

Do MCP tools need the same discipline?

Yes. inputSchema should close extra keys and list every required field. Subagents inherit MCP, not functions. The door to your orders stays on the main agent.

Will a failed check stall the session?

Write a tool_result on the same turn_id / call_id pair. A clear error lets the model retry with new arguments. Do not open a new session as a retry.

Conclusion

Stable JSON on the Agents API is not a longer prompt. It is three doors: tool parameters, a host check, and a separate Schema for the final card. Execution happens after the gate.

Write down which rule A-1042 would fail. If you can point at a path, the data is verifiable. Models change; the doors should settle first.

Further reading

Changelog: First published