JSON Schema for LLM Function & Tool Calling (OpenAI + Anthropic + Gemini)
Last updated:
Every modern LLM provider ships a way to let the model call your code: OpenAI calls it tools, Anthropic calls it tool_use, Google calls it functionDeclarations. Underneath the different field names, all three speak the same language — a JSON Schema describing what arguments the model should produce. The schema is the wire contract; the description text on each field is the instruction the model reads to decide what to put in it. This guide walks through the shared mental model, the per-provider differences (envelope shape, supported keywords, strict mode), and the description-quality patterns that separate tool definitions the model calls correctly from ones it ignores or fills with garbage. It assumes you have written basic JSON Schema before; if not, start with our JSON Schema basics guide first.
Building a tool schema and want to check it parses cleanly before you ship it to the model? Paste it into Jsonic's JSON Schema Validator — it catches typos in keyword names, type mismatches, and broken $ref pointers before the LLM does.
What 'function calling' actually means across LLM providers
The vocabulary is messy. OpenAI shipped the feature as "function calling" in June 2023, then renamed the wire format to "tools" in mid-2024. Anthropic shipped Claude with "tool use" from the start and never used the function word in its API. Google Gemini calls it "function calling" but accepts the parameter name tools on the request side. Under all three vocabularies, the mechanic is the same.
The model does not execute your code. It emits a structured request — a JSON object with a tool name and an arguments payload — that you receive in the response. Your application executes the call, captures the result, and sends the result back to the model in a follow-up turn. The model then continues its reasoning with the result in hand. The loop is: user prompt → model emits tool call → you execute and return result → model emits final answer (or another tool call).
The JSON Schema you provide is what tells the model two things at once: what operations are available (one tool per schema, with a name and description), and what the argument payload should look like (the schema body). Every provider validates the model output against this schema to some degree — OpenAI strict mode does it at the token sampler, Claude and Gemini do it as a post-emission check — but in every case the schema is the contract that defines what counts as a valid call.
The shared mental model: tool name + description + JSON Schema parameters
Every provider asks for the same three pieces of information per tool:
- Name — a short identifier in snake_case (
get_weather,search_products,send_email). This is what shows up in the tool-call response. - Description — a 1–3 sentence string that tells the model when to use this tool and what it returns. The model reads this as part of its prompt.
- Parameters schema — a JSON Schema object describing the arguments the model should produce. Usually
type: "object"with apropertiesmap.
The differences across providers are only in the envelope. OpenAI wraps each tool in { "type": "function", "function": { name, description, parameters } } and uses the field name parameters. Anthropic uses the flatter { name, description, input_schema } shape with the field name input_schema. Gemini uses { functionDeclarations: [{ name, description, parameters }] }. Once you set up an adapter layer, the same schema body flows through all three.
The model reads the entire schema, not just the parameter names. description on the schema root tells it when to call the tool; description on each property tells it what to put there; enum values constrain the choices; required tells it which fields it cannot skip. Treat the schema like a docstring for a function you are publishing — it is the only documentation the model has.
OpenAI: tools array with type 'function' (replaces deprecated 'functions')
OpenAI's current shape, in use since June 2024, wraps each tool in an outer object with a type discriminator. The legacy functions parameter still works but no new features land on it — strict mode, parallel calls, and Structured Outputs integration are all tools-only.
from openai import OpenAI
client = OpenAI()
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city. Use when the user asks about weather, temperature, or conditions for a specific location.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'San Francisco' or 'Tokyo'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default to celsius unless the user is in the US."
}
},
"required": ["city", "unit"],
"additionalProperties": False
},
"strict": True
}
}]
response = client.chat.completions.create(
model="gpt-4o",
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
tools=tools,
tool_choice="auto",
)The strict: true flag inside the function object opts into constrained-decoding mode — the sampler is restricted at the token level so the output is guaranteed to match the schema. Strict mode requires three things: additionalProperties: false on every object level, every property listed in required, and a restricted set of JSON Schema keywords (no format, no minimum/maximum, no pattern). For non-strict mode, the schema is still useful but loose — the model is fine-tuned to emit tool calls but not constrained at the sampler.
For broader background on getting JSON out of OpenAI specifically, see our OpenAI JSON mode guide and the deeper Structured Outputs reference.
Anthropic: tools array with input_schema
Anthropic uses a flatter envelope: each tool is { name, description, input_schema } with no type discriminator. The field name input_schema (rather than parameters) is the only naming difference from OpenAI.
import anthropic
client = anthropic.Anthropic()
tools = [{
"name": "get_weather",
"description": "Get current weather for a city. Use when the user asks about weather, temperature, or conditions for a specific location.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'San Francisco' or 'Tokyo'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default to celsius unless the user is in the US."
}
},
"required": ["city"]
}
}]
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)Claude accepts the broadest JSON Schema dialect of the three providers. It supports string formats (date-time, email, uri), minimum/maximum on numbers, pattern on strings, $ref for shared definitions, and the standard composition keywords (anyOf, oneOf, allOf). It does not require additionalProperties: false — extra fields the model produces are accepted but ignored.
Claude responds with content blocks. Tool calls appear as blocks of type tool_use with name, id, and input fields. Parallel calls produce multiple tool_use blocks in one assistant message — the default behavior. See our Claude tool use deep dive for the full request/response shapes and conversation-loop patterns.
Gemini: functionDeclarations with OpenAPI-flavored schema
Google Gemini wraps tool definitions under a functionDeclarations key inside the tools array. The schema dialect is OpenAPI 3.0 — a JSON Schema subset with slightly different naming on a few keywords.
from google import genai
client = genai.Client()
tools = [{
"functionDeclarations": [{
"name": "get_weather",
"description": "Get current weather for a city. Use when the user asks about weather, temperature, or conditions for a specific location.",
"parameters": {
"type": "OBJECT",
"properties": {
"city": {
"type": "STRING",
"description": "City name, e.g. 'San Francisco' or 'Tokyo'"
},
"unit": {
"type": "STRING",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default to celsius unless the user is in the US."
}
},
"required": ["city"]
}
}]
}]
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="What's the weather in Paris?",
config={"tools": tools},
)Gemini-specific gotchas to know about:
- Uppercase types. The OpenAPI dialect Gemini uses expects type values in uppercase (
OBJECT,STRING,NUMBER,ARRAY,BOOLEAN). The Python SDK accepts lowercase and converts; the raw REST API expects uppercase. - No external
$ref. Gemini accepts inline$refreferences within the same schema but rejects external references. - Limited
patternsupport. Some regex syntax is rejected. Use enums where possible. nullable: true. Gemini follows the OpenAPI 3.0 convention of anullableboolean rather than the JSON Schema convention oftype: ["string", "null"].
Despite the differences, the same shape works in practice: a JSON Schema object with type, properties, required, and description fields per property is accepted by all three providers.
Schema subset compatibility: which JSON Schema keywords work where
The intersection of supported JSON Schema across the three providers is large but not total. The matrix below covers the keywords that come up most often in tool definitions.
| Feature | OpenAI strict | OpenAI loose | Claude | Gemini |
|---|---|---|---|---|
| Required fields enforced | Yes (every property) | Best-effort | Best-effort | Best-effort |
additionalProperties: false required | Yes | No | No | No |
anyOf | Yes | Yes | Yes | Yes |
oneOf | No (use anyOf) | Yes | Yes | Limited |
Nested $ref | Yes (internal) | Best-effort | Yes | Internal only |
String formats (date-time, email) | No | No | Yes | Yes |
minimum / maximum | No | Best-effort | Yes | Yes |
pattern (regex) | No | Best-effort | Yes | Limited |
enum | Yes | Yes | Yes | Yes |
| Nested objects | Yes | Yes | Yes | Yes |
Arrays with items | Yes | Yes | Yes | Yes |
The pragmatic strategy: design to the strictest target you care about (usually OpenAI strict mode), then ship that same schema to the other providers. They accept stricter schemas without complaint — the only one-way conversion is type-case for Gemini if you call the raw REST API. Tools like Pydantic and Zod validation can generate JSON Schema that targets the OpenAI strict subset by default.
Description fields: how docstring quality affects accuracy
The description text on the tool and on each parameter is the single biggest accuracy lever in your tool definition. The model treats descriptions as part of its prompt — vague descriptions produce vague tool calls, precise descriptions produce precise ones. Spend the same care on description text that you would spend on a public API docstring.
What to write at the tool level (50–200 characters):
- When to use it — the trigger that should cause the model to pick this tool. "Use when the user asks about current or forecast weather" beats "Gets weather."
- What it returns — one sentence on the response shape so the model knows what to expect on the next turn.
- When NOT to use it — an explicit boundary against confusion. "Do not use for historical weather; use
get_weather_historyinstead."
What to write at the parameter level (50–200 characters):
- Format and units — "ISO 8601 date string, e.g. 2026-05-23" rather than "the date."
- Allowed values — if there is an
enum, explain what each value means. If there is not, give an example. - Optionality trigger — for optional parameters, state when the model should include the field. "Include only if the user specifies a currency other than USD."
Anthropic explicitly recommends including a short example payload in the tool description for high-stakes tools. The model treats examples as gold-standard evidence of intent. A two-sentence description with one example often outperforms a five-sentence description without.
Multi-tool selection, tool_choice, and parallel calls
All three providers expose two orthogonal controls: which tool (if any) the model should call, and whether it can call several in parallel.
OpenAI tool_choice:
"auto"(default) — model decides whether to call a tool, and which one."none"— model is not allowed to call any tool this turn."required"— model must call exactly one tool from the array.{ "type": "function", "function": { "name": "get_weather" } }— force a specific tool.parallel_tool_calls: true(default) — model may emit multiple tool calls in one response.
Anthropic tool_choice:
{ "type": "auto" }(default) — model decides.{ "type": "any" }— model must call some tool.{ "type": "tool", "name": "get_weather" }— force a specific tool.disable_parallel_tool_use: false(default) — parallel calls allowed.
Gemini functionCallingConfig.mode:
"AUTO"(default),"ANY","NONE"— same semantics.allowedFunctionNames— restrict to a subset of declared tools.
Parallel calls are the default everywhere. The pattern: the model emits two or more tool calls in one assistant message, you execute them (often concurrently with asyncio.gather or Promise.all), then send back one tool result per call. Disable parallel mode only when calls are dependent — when the second call needs the first call's output as input. For broader patterns on getting reliable JSON out of LLMs, see our LLM JSON output strategies reference.
Reusing one JSON Schema across all three providers
Because the schema body is the same shape on every provider, you can declare one JSON Schema and adapt the envelope at request time. A pattern that works in production:
# Shared schema (designed to the OpenAI strict subset for maximum portability)
GET_WEATHER_SCHEMA = {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. 'San Francisco' or 'Tokyo'"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default to celsius."
}
},
"required": ["city", "unit"],
"additionalProperties": False
}
TOOL_NAME = "get_weather"
TOOL_DESC = "Get current weather for a city. Use when the user asks about weather, temperature, or conditions for a specific location."
# OpenAI envelope
openai_tools = [{
"type": "function",
"function": {
"name": TOOL_NAME,
"description": TOOL_DESC,
"parameters": GET_WEATHER_SCHEMA,
"strict": True
}
}]
# Anthropic envelope
anthropic_tools = [{
"name": TOOL_NAME,
"description": TOOL_DESC,
"input_schema": GET_WEATHER_SCHEMA
}]
# Gemini envelope
gemini_tools = [{
"functionDeclarations": [{
"name": TOOL_NAME,
"description": TOOL_DESC,
"parameters": GET_WEATHER_SCHEMA
}]
}]For Pydantic users, the same pattern works one level higher — define a Pydantic model and let model.model_json_schema() generate the schema. The same schema flows into all three envelopes.
from pydantic import BaseModel, Field
from typing import Literal
class GetWeatherArgs(BaseModel):
city: str = Field(description="City name, e.g. 'San Francisco' or 'Tokyo'")
unit: Literal["celsius", "fahrenheit"] = Field(
description="Temperature unit. Default to celsius."
)
# Generates the JSON Schema, ready for any provider's tools array
schema = GetWeatherArgs.model_json_schema()Key terms
- function calling / tool calling / tool use
- The same mechanic across providers: the model emits a structured request for your code to execute, you run it locally, and return the result for the model to continue reasoning. OpenAI says "tools," Anthropic says "tool_use," Gemini says "function calling" — wire format differs, behavior is the same.
- strict mode (OpenAI)
- A flag (
strict: true) that switches OpenAI's tool calling to constrained decoding — the sampler is restricted at the token level so the output is guaranteed to match the schema. RequiresadditionalProperties: falseand every property inrequired. input_schema(Anthropic)- Anthropic's field name for the JSON Schema describing tool arguments. Equivalent to OpenAI's
parametersfield. Accepts the broadest JSON Schema dialect of the three providers. functionDeclarations(Gemini)- Gemini's wrapper key for tool definitions, nested inside the
toolsarray. Uses an OpenAPI 3.0 schema dialect with uppercase type names in the raw REST format. - parallel tool calls
- The model emitting multiple tool calls in one assistant message. Default on for all three providers. Disable when calls are dependent on each other's output.
tool_choice- The request parameter that controls whether the model is allowed, required, or forbidden from calling a tool this turn. Values include auto, none, required, or a specific tool name.
Frequently asked questions
What is the difference between function calling and tool calling?
They describe the same thing with different vocabularies. OpenAI originally shipped the feature as "function calling" in 2023, then renamed the wire format to "tools" in mid-2024 — the parameter changed from functions to tools and each entry is now wrapped as { "type": "function", "function": { ... } }. Anthropic shipped Claude with "tool use" from day one and never used the function vocabulary. Google Gemini calls it "function calling" but uses the parameter name tools as well. The model behavior is identical across providers: you describe callable operations with JSON Schema, the model decides whether to call one (or several), and you execute the call locally and feed the result back. Whenever someone says "tool calling," "function calling," or "tool use," assume the underlying mechanic is the same — only the API field names differ.
Is OpenAI 'functions' deprecated?
Yes. The legacy functions and function_call parameters on chat completions were deprecated in June 2024 in favor of tools and tool_choice. The old fields still work for backward compatibility, but every new feature — strict mode, parallel tool calls, structured outputs integration — lands on the new tools format only. If you are writing new code, use tools=[{"type": "function", "function": { "name": ..., "description": ..., "parameters": ... }}] and tool_choice="auto" (or "required" or { "type": "function", "function": { "name": "..." } }). The schema you put inside parameters is the same JSON Schema you would have put inside functions[i].parameters — only the envelope changed. Migration is mechanical: wrap each function object in { "type": "function", "function": <old object> } and rename the request parameter.
Why does the LLM ignore my optional parameters?
Two causes account for almost every case. First, optional parameters with weak or missing description fields read as "the model can probably skip this." Models treat the description as the instruction; if the description does not say when the parameter matters, the model defaults to omitting it. Rewrite descriptions to spell out the trigger: "Include only when the user specifies a currency other than USD" rather than "the currency." Second, on OpenAI strict mode, every property listed in properties must also appear in required — strict mode does not honor traditional optional parameters. To get an optional-style field under strict mode, declare the type as a union with null (anyOf with { "type": "null" }) and keep the field required, then treat null as "not provided" in your handler. Claude and Gemini do not have this constraint, but the description-quality issue applies everywhere.
Can I use the same JSON Schema across OpenAI, Claude, and Gemini?
Mostly yes, with caveats. The intersection of supported keywords is large: type ("object", "string", "number", "integer", "boolean", "array"), properties, required, items, enum, description, and anyOf all work across all three providers. The differences are at the edges. OpenAI strict mode requires additionalProperties: false on every object level and disallows certain keywords (format constraints, minimum, maximum, pattern). Claude is the most permissive — it accepts the broadest JSON Schema dialect including string formats like date-time and email. Gemini uses an OpenAPI-flavored subset that drops a few keywords (no $ref to external schemas, limited pattern support). A pragmatic strategy is to write the schema in the strictest dialect you need (usually OpenAI strict), then send that same schema to the other providers — they will accept the stricter version even when their own minimum is looser.
How important is the description field on each parameter?
It is the single biggest lever on accuracy. The model reads the function description and each parameter description as part of its prompt — they directly shape when the model calls the function and what it puts in each field. Vague descriptions cause the model to fabricate values, skip optional parameters, or call the wrong function. The sweet spot is roughly 50 to 200 characters per parameter: long enough to specify units, format, allowed values, and the trigger ("when the user mentions a date"), short enough that the entire schema does not bloat the context. Anthropic explicitly recommends including examples in tool descriptions for high-stakes tools. Spend the same care on description text that you would spend on type signatures in a public API — these strings are the public interface between your code and the model.
Does function calling require a paid model?
No, but support varies. OpenAI offers function calling on every chat-completions-capable model including the free-tier gpt-4o-mini, and structured outputs (strict mode) is GA on gpt-4o, gpt-4o-mini, and o-series models. Anthropic supports tool use on every Claude model from Haiku to Opus including the free-tier Haiku via the Anthropic API. Google Gemini supports function calling on gemini-2.0-flash (free tier on the AI Studio) and all paid Gemini models. Open-weight models are more variable: Llama 3.1 405B, Mistral Large 2, and Qwen 2.5 ship native tool-calling fine-tunes, while smaller models often require structured-output prompting hacks instead. If you are choosing a model specifically for tool use accuracy, OpenAI strict mode and Claude Sonnet 3.5+ are the current accuracy leaders.
Can a function be called multiple times in one response?
Yes — this is "parallel tool calls" and all three major providers support it. OpenAI emits multiple entries in the tool_calls array within a single assistant message; you execute them in any order (often in parallel) and reply with one tool message per call, each referencing its tool_call_id. The behavior is on by default; pass parallel_tool_calls: false to force one-at-a-time. Claude is parallel by default — content blocks of type "tool_use" can appear several times in one assistant message — and offers a disable_parallel_tool_use option for cases where ordering matters. Gemini emits multiple functionCall parts in a single Content. Use parallel calls for independent lookups (fetch weather for three cities, look up two products). Disable when calls depend on each other — for example, when the second call requires the first call result as input.
Why does Structured Outputs require additionalProperties: false but function calling does not?
Because they use different validation paths. OpenAI Structured Outputs (the response_format strict mode for plain JSON output) is implemented by constrained decoding — the sampler is restricted at the token level so the model can only emit characters that keep the partial output a valid prefix of the schema. That sampler needs a closed schema with no ambiguity, which is why additionalProperties: false is mandatory and every property must be listed in required. Function calling without strict mode (the default before mid-2024) is implemented as a prompt-engineering pass: the model is fine-tuned to emit a tool-call JSON, but there is no token-level constraint, so loose schemas are tolerated. Strict mode on tools (added in August 2024) brings the same constrained-decoding guarantee to function calling and inherits the same requirements: additionalProperties: false and full required arrays.
Further reading and primary sources
- OpenAI Docs — Function Calling — Current OpenAI reference for the tools parameter, strict mode, and tool_choice
- Anthropic Docs — Tool Use — Claude tool use overview, including input_schema and parallel tool calls
- Google AI — Function Calling with Gemini — Gemini functionDeclarations, OpenAPI schema dialect, and functionCallingConfig modes
- JSON Schema Specification — The full JSON Schema spec — useful when you need a keyword the providers all support
- OpenAI Structured Outputs — The constrained-decoding mode that backs strict tools and JSON response_format