Tool Calling Standards: JSON Schema

P2
Deep Dive · Protocols & Interop

Function/tool calling standards: JSON Schema as the shared substrate.

Every major model provider grounds tool calling in the same idea: describe a function with JSON Schema, let the model emit a structured call, execute it, feed the result back. This essay dissects that contract, the schema dialect that actually matters in practice, and where providers genuinely differ versus where they only rename things.

STEP 1

The universal contract.

Strip away provider branding and tool calling is four steps that have been stable since 2023:

  • Declare. The caller sends the model a list of tools, each with a name, a natural-language description, and a JSON Schema for its parameters.
  • Select. The model, given the conversation, decides whether to call a tool and emits a structured call: a tool name plus a JSON arguments object intended to validate against that tool's schema.
  • Execute. Your code — never the model — runs the actual function with those arguments and produces a result.
  • Return. The result is appended to the conversation, correlated to the call by an opaque ID, and the loop continues until the model answers without calling a tool.

The model never executes anything. It only produces a request to call a function. This separation is the entire security and reliability story of tool calling: the schema constrains what the model can ask for, and your handler decides what actually happens.

STEP 2

JSON Schema is the lingua franca — but a subset of it.

The shared substrate across OpenAI, Anthropic, Google, and essentially every open-model tool-calling convention is JSON Schema (the IETF draft family). A tool's parameters are a JSON Schema object with typed properties, a required list, and optional constraints like enum, minimum, format, and nested objects/arrays.

# A provider-neutral tool schema (the part everyone agrees on)
{
  "name": "create_ticket",
  "description": "Open a support ticket. Use only after the "
                 "user has confirmed the summary.",
  "parameters": {
    "type": "object",
    "properties": {
      "title":    {"type": "string", "maxLength": 120},
      "severity": {"type": "string",
                    "enum": ["low", "normal", "high", "urgent"]},
      "component": {"type": "string"},
      "steps": {
        "type": "array",
        "items": {"type": "string"},
        "description": "Ordered reproduction steps."
      }
    },
    "required": ["title", "severity"],
    "additionalProperties": false
  }
}

The practical caveat: providers support a subset of full JSON Schema, and the subsets are not identical. Exotic constructs — $ref recursion, oneOf/allOf composition, patternProperties, conditional if/then — may be silently ignored, rejected, or only honoured in a "strict" mode. The safe interoperable core is: scalar types, enum, arrays, nested objects, required, and clear descriptions. Treat anything beyond that as provider-specific until verified.

The single highest-leverage field is description — for the tool and every parameter. The model selects and fills tools largely from prose, not from types. A precise description ("ISO-8601 date; omit for 'today'") prevents more bad calls than any structural constraint.

STEP 3

Where providers differ: the envelope, not the schema.

The schema body is portable. The wrapper around it is not. The Field Guide's Tool Use chapter walks the on-the-wire shapes in detail; here is the protocol-level summary:

# Anthropic Messages API — tool declaration
{
  "name": "create_ticket",
  "description": "Open a support ticket.",
  "input_schema": { /* the JSON Schema object */ }
}
# Model emits a `tool_use` content block:
#   {type:"tool_use", id:"toolu_…", name, input:{…}}
# You return a `tool_result` block keyed by tool_use_id.
# OpenAI Responses API — tool declaration
{
  "type": "function",
  "name": "create_ticket",
  "description": "Open a support ticket.",
  "parameters": { /* the JSON Schema object */ }
}
# Model emits a `function_call` item with stringified
# `arguments`; you return a `function_call_output`
# item keyed by call_id.

The differences that actually matter for an interop layer:

  • Key naming. input_schema vs parameters; input (parsed object) vs arguments (JSON string you must parse).
  • The type discriminator. OpenAI tags tools with "type": "function" because it also offers built-in tools (web search, file search, computer use) alongside user functions. Anthropic has one tool kind, so no discriminator.
  • Strict mode. OpenAI's "strict": true with additionalProperties: false constrains decoding so arguments provably match the schema. Anthropic enforces conformance differently and does not need this flag. Strict mode also narrows the accepted schema subset — a deliberate trade of expressiveness for guarantees.
  • Correlation IDs. tool_use_id vs call_id — same concept (match a result to its call, essential for parallel calls), different name.
  • History model. Anthropic's strict role-alternating messages with content blocks vs OpenAI's flat list of typed input items correlated by ID.
STEP 4

An adapter is small because the core is shared.

Because the schema is portable and only the envelope differs, a provider-neutral tool definition plus thin per-provider adapters is the standard interop pattern. You author each tool once, in a neutral form, and render it into each provider's wrapper.

# One neutral definition, rendered per provider
def to_anthropic(t):
    return {"name": t.name,
            "description": t.description,
            "input_schema": t.schema}

def to_openai(t):
    return {"type": "function",
            "name": t.name,
            "description": t.description,
            "parameters": t.schema,
            "strict": True}
# Same t.schema (the interoperable JSON Schema core)
# feeds both. Parsing the call back is the only
# other place the envelope leaks.

This is precisely the seam that the Model Context Protocol formalises: instead of every agent author writing these adapters and every tool author redescribing their function per provider, MCP makes the tool description a protocol artefact a server publishes once. The next essays follow that thread. The takeaway here: tool calling has a real, stable standard at its core — JSON Schema plus a four-step request/execute/return loop — and the provider differences are a thin, well-understood translation layer, not a fundamental incompatibility.

Schema-valid is not the same as safe. The schema bounds the shape of arguments, never their intent. {"path": "../../etc/passwd"} is perfectly valid against {"type":"string"}. Argument validation against business rules and authorization happens in your handler — see the Safety & Agentic Security track for the threat model.