Development

A Tool Schema a Model Can Call Correctly

Antonio Evans9 min read

A split-panel diagram on a white background.
Key points

Key points

  1. 1Write the JSON Schema as a strict contract: every field the function truly needs is in required, closed value sets are enum, and additionalProperties is false.
  2. 2Replace free-text string fields with enum or pattern wherever the valid set is finite, and give each field a short description plus one or two concrete examples.
  3. 3Test the schema itself, not the prompt: generate 20 to 50 calls from a fixed prompt set, run them through the same validator production uses, and count failures per field.
  4. 4Fix the schema where failures cluster; a field that fails often is a schema bug, and a field that fails once is model variance.

The request half of a tool interface

Most tool-calling writing covers what the model emits: the arguments, the JSON, the errors. The half that decides whether a call survives contact with production is the request side, the schema you hand the model before it says anything. Treat that schema as a contract with strict types, required fields, format checks and no extraneous fields, and you convert a text generator into a component with a typed interface, as Adaline's guide to tool calling functions puts it.

The failure modes repeat. A model omits a required parameter, returns a string where an integer belongs, invents a value outside the allowed set, or adds a field the function never declared. Without a validation layer each of those reaches application logic, breaking workflows and creating security risk, which is why a walkthrough of JSON Schema validation for tool calls argues the schema should be checked before execution, never after.

The claim worth internalizing: roughly half of tool call errors are the model's fault, and the other half are bugs in the signature you handed it. That is the argument in David Rodenas's account of validation and tool call errors, and it reframes debugging. When a call fails validation, the first question is not how to prompt better but what in this schema let an invalid value look reasonable to the model.

A second reframe matters for anyone operating agents. Not every failure is retryable the same way. Transient transport errors belong to the orchestration layer, while validation failures cannot be fixed by resending the same payload, so the model has to read the error and produce a corrected request, as n8n's guide to LLM tool calling error handling lays out. The quality of your schema therefore decides how much recovery burden lands on the model, and whether it can recover at all.

Designing the schema: required, types and additionalProperties

Start with the fields the function cannot run without and mark every one of them required. A schema that lists properties but requires none tells the model those fields are optional, so it will omit them. The Adaline example of a free/busy tool makes the pattern concrete: a non-empty array of email-formatted attendees, ISO 8601 start and end datetimes, a timezone, additionalProperties: false to block invented fields, and a strict flag enforcing validation before side effects (Adaline Labs).

Types do the next layer of work. integer versus string, array with minItems, format: date-time and format: email all narrow the space of outputs the model can produce. A model that must emit a datetime in a defined format has far fewer ways to be subtly wrong than one that can write any string at all.

Then close the object. additionalProperties: false is the cheapest line in the schema: it stops the model from adding a plausible-sounding field your handler will silently ignore, and it makes the acceptance criterion unambiguous during validation.

A practical order for writing a schema from scratch:

StepWhat to writeWhy it prevents errors
1required list for every field the handler readsRemoves the "field missing" class entirely
2Type, format and numeric bounds per propertyRejects wrong types before code runs
3enum for any field with a closed value setEliminates invented categories
4One or two short examples per fieldAnchors format and edge cases
5additionalProperties: falseBlocks phantom fields
6A short description on the tool and each fieldReduces misreads of intent, not just shape
7A validator run against generated callsProves steps 1 to 6 actually hold

The last row is the one teams skip, and the one that matters most.

Enums, patterns and defaults: closing the value set

Wherever the set of valid values is finite, replace free text with enum. Category, priority, channel, status, unit of measurement, action to take: each becomes a fixed list the model must choose from or fail. The OpenAI community thread on enum validation in function calls documents the exact failure an enum fixes: a model asked to categorize into a list occasionally returned values outside the list, and the fix was to put the enum in the schema rather than in the prompt.

Pydantic and JSON Schema both make this cheap. A schema with "priority": {"type": "string", "enum": ["low", "normal", "high", "urgent"]} leaves the model four choices. A schema with "priority": {"type": "string"} leaves a near-infinite space of strings, most of them wrong.

pattern covers string fields that cannot be an enum but can still be constrained: identifiers, slugs, ticket references, currency codes. A pattern does not guarantee correctness, but it eliminates the malformed-input class.

Defaults deserve a warning. A JSON Schema default is not applied by most validators; it is documentation. If a field is genuinely optional, make the default explicit in the tool description and in the handler, and consider making the field required with an explicit sentinel instead. Models fill optional fields inconsistently, and a default living only in the schema will eventually produce a call that omits the field and surprises the handler.

Examples per field are the underrated tool. A one-line example of a well-formed value, placed in the field description, reduces formatting errors without changing type constraints. The Agenta guide to structured outputs and function calling makes the broader point that structured output approaches should be chosen for reliability rather than post-hoc cleanup, and a good example does the same job at the schema level.

One warning from the field: an enum inside the prompt is not an enum in the schema. The model can still disobey prose. Only the schema, plus a validator, makes it a hard constraint.

Validation harness: 20 to 50 calls, one validator, failures per field

The central claim here is that you should test the schema, not just the prompt. The method is small enough to run before lunch and produces a number you can compare across schema revisions.

  1. Fix a prompt set of 20 to 50 representative user requests, written before you run anything, covering the happy path and the awkward ones (missing context, ambiguous values that should map onto a specific enum member).
  2. Generate one tool call per prompt against the current schema, storing the raw arguments.
  3. Run every call through the same validator the production API uses, whether jsonschema, Pydantic or your provider's strict mode.
  4. Count failures per field, not just overall.

A minimal harness:

import jsonschema

def run_harness(prompts, generate_call, schema):
    failures = {}
    total = 0
    for prompt in prompts:
        total += 1
        args = generate_call(prompt, schema)
        try:
            jsonschema.validate(instance=args, schema=schema)
        except jsonschema.ValidationError as err:
            field = err.absolute_path[0] if err.absolute_path else "<root>"
            failures[field] = failures.get(field, 0) + 1
    return total, failures

The scoring is the point. Suppose 40 prompts produce 12 invalid calls. If 9 of the 12 fail on window_end, the model is not confused; the schema is. Maybe the description does not state the timezone, or the format is unclear. Fix the schema. If 3 fail on different fields once each, that residue is model variance, and prompt work or a retry path is the right investment.

Two calibration notes. First, validation alone fixes only part of the problem; the schema is what fixes the rest, per the Rodenas account. Second, keep the harness cheap and repeatable: it is a regression test for the interface, not a metric to optimize.

When validation fails: routing errors to the right layer

The harness tells you whether the schema holds. The next question is what the runtime should do when a call still slips through. The answer is routing, not universal retry.

A 400 or 422 on an argument means the payload is structurally wrong, so resending it cannot help; the model needs to see the error and correct it. That is the split n8n's production failure categories describe: transient transport errors to the orchestrator, validation failures back to the model. The Temporal HTTP retry recipe makes the same distinction at the HTTP level, treating 400, 401, 403, 404 and 422 as non-retryable and retrying only transient codes such as 408, 429, 500, 502, 503 and 504.

For the retryable side, do not guess. A well-formed 429 carries a Retry-After header and typically a Problem Details body, and Zuplo's guide to HTTP 429 recommends reading that header, using exponential backoff with jitter and making sure retries are idempotent. The same reasoning applies when an agent hits a rate limit on a tool call rather than an LLM call: the retry policy belongs to the transport, not to the model's reasoning.

For the non-retryable side, return a structured error the model can act on. A response like {"error": "validation failed"} gives an agent nothing to work with, as Zuplo's guide to API readiness for agents argues, while a stable error code, a human-readable explanation, recovery hints and the failing field name turn a dead end into a self-correction. Nordic APIs' piece on designing API error messages for AI agents recommends RFC 7807 Problem Details for exactly this reason, and Cloudflare's RFC 9457 agent error pages shows the payoff: stable keys like retryable and retry_after let an agent classify a failure instead of parsing prose, and structured responses cut payload size and token usage by more than 98% versus HTML on a rate-limit error.

The SmartBear walkthrough of OpenAPI error models makes the design point for the spec side: define one reusable error schema in components and reference it from every status, so clients and agents never guess at a new shape. Do the same for tool results.

A worked example: same tool, two schemas

Take a booking tool: book_meeting(attendees, start, end, room, priority, notes).

A loose schema declares attendees as a string, start and end as strings, room and priority as strings, and notes as a string, with only attendees required. On a 40-prompt harness, suppose 14 calls fail. Six pass a single email as attendees instead of an array. Four pass dates in a local format instead of ISO 8601. Two set priority to "important" rather than "high". One sends an extra timezone field. One sends "next Tuesday" as start.

A strict schema changes the same tool: attendees is an array of format: email with minItems: 1; start and end are format: date-time; room is an enum of the ten bookable rooms; priority is an enum of four values; notes is optional and length-limited; all but notes are required; additionalProperties is false. The same harness now fails 3 of 40, and those three cluster on start for prompts that contain no date at all. That cluster is not a schema bug; it is a case the tool cannot resolve, and the right fix is a structured error telling the model to ask the user for a date.

The counts are illustrative, but the decision rule they encode is stable: after one strict pass, failures should concentrate on fields whose values are genuinely unknowable from the prompt. If they still cluster on fields with a closed value set, your enum is incomplete or your description is ambiguous. Run the same harness against whichever model serves the call, because the schema is the constant and the model is the variable; if you route across models, Quanta Inference exposes text, image, video, audio and embedding models through one OpenAI-compatible key, which keeps the variable isolated.

Keeping the schema honest after it passes

Once the harness passes, the work shifts to observation. The first failure layer in production agents is LLM API failures such as 429, 408 and 500, and the recovery belongs to orchestration rather than to the prompt, as the Towards AI guide to retries in agents describes. A large share of what looks like a schema problem at scale is really a transport problem, so log both.

Log the raw arguments, the validator outcome and the failing field per call, so the per-field failure table from the harness becomes a dashboard rather than a one-off script. Then keep the schema under review the way you keep an API under review: a model swap or a prompt rewrite changes failure rates, and the Agenta guide notes that a prompt that works in testing can regress after a model update. The harness is the cheap insurance.

Two operational habits finish the job. First, version schemas and record which version each logged call ran against, so a failure spike maps to a change. Second, treat a schema change as an interface change: bump it, rerun the harness and compare per-field failure counts before shipping. A schema with no version is an interface you cannot audit.

FAQ

FAQ

Why is testing the schema different from testing the prompt?

A prompt test measures whether the model understands the request. A schema test measures whether the interface accepts or rejects what the model produces, which is what decides production reliability. Per-field failure counts tell you whether a problem is structural or behavioral, and only the structural ones are fixed by editing the schema, as Rodenas argues.

How many prompts do I need in the harness?

20 to 50 is enough to see clusters if the schema has a real defect, because a genuine schema bug fails many calls, not one. More important than count is representativeness: include bare prompts with missing context, ambiguous values that map onto enum members, and prompts with a clear correct answer. Rerun the same set after every schema edit so the comparison is honest.

Should optional fields exist at all?

Sometimes, but make the optionality real in the handler, not just the schema. Most validators do not apply a JSON Schema default, so an omitted field reaches your code as absent, not defaulted. If a field is optional, say in the description what happens when it is missing and have the handler supply the fallback explicitly rather than relying on the schema to do it.

What do I return to the model when validation fails?

Return structured, actionable detail: a stable error code, the failing field name, what was expected and a recovery hint, preferably in a standard shape like RFC 9457 Problem Details, as Nordic APIs and Cloudflare recommend. A bare "validation failed" forces the model to guess, and Zuplo's agent API guide is right that an agent cannot reason about a failure it cannot see.

Which failures should be retried automatically and which should go back to the model?

Rate limits, timeouts and transient 5xx belong to the orchestration layer, with Retry-After respected and jittered backoff, as n8n's failure categories and the Temporal HTTP retry recipe both describe. Argument validation failures and logic errors belong to the model, which has to read the error and change the request rather than repeat it. Blurring the two is the fastest way to build an agent that retries a malformed payload forever.

Sources

Sources

  1. Writing Effective Tool Calling Functions - Adaline Labs
  2. 4.3, What is JSON Schema Validation? | by John Chiwai
  3. Validation Fixed Half of My Tool Call Errors
  4. LLM Tool Calling Error Handling: Retries and Fallbacks
  5. Don't understand how to add enum validation to function call
  6. The guide to structured outputs and function calling with ...
  7. Retry policy from HTTP responses | AI Cookbook
  8. HTTP 429 Too Many Requests: Causes, Headers & Retry ...
  9. How to Design APIs That AI Agents Can Actually Use
  10. Designing API Error Messages for AI Agents
  11. Slashing agent token costs by 98% with RFC 9457 ...
  12. Designing error models in OpenAPI for agent-safe APIs
  13. Building Retries in Agents
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles