Figures on this page were verified 31 August 2026 against the providers' own documentation. Pricing, context windows and rate limits change without notice, so confirm any number against the provider before you rely on it. Tell us if something here is out of date.
When a tool call arrives with arguments you cannot parse, the model is almost never inventing malformed JSON at random. Three causes account for nearly all of it: the arguments were truncated by max_tokens, your schema permitted something you did not intend, or you are parsing a streamed call before it has finished arriving. Each has a different fix, and only one of them is a prompting problem.
What you actually receive
Both providers return tool arguments as a string that you are expected to parse, not as a nested object. OpenAI returns tool_calls[].function.arguments as serialised JSON; Anthropic returns a tool_use block whose input is already an object but which can still be incomplete if generation stopped early.
// OpenAI: arguments is a STRING, and json.loads can fail on it
{
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_orders",
"arguments": "{"customer_id": "C-4471", "status": "shi"
}
}]
}That example is the signature of cause one. The JSON is not wrong, it is unfinished. Nothing in the parse error will tell you that; you have to look at the stop reason.
Cause 1: the call was truncated
Tool arguments are generated tokens like any other. If the response hits the output ceiling mid-argument, you get a fragment. Check the stop reason before you parse.
choice = response.choices[0]
if choice.finish_reason == "length": # OpenAI
raise Truncated("raise max_tokens or shrink the schema")
# Anthropic
if response.stop_reason == "max_tokens":
raise Truncated("tool input is incomplete")
args = json.loads(choice.message.tool_calls[0].function.arguments)The usual trigger is a schema with a free-text field. One description or query property with no length guidance invites the model to write several hundred tokens into it. See why responses get cut off at max_tokens for the general case.
Cause 2: your schema allowed it
A permissive schema is the most common source of “wrong” arguments that parse perfectly well. If a property has no enum, no type, or is not listed in required, the model is free to omit it or fill it with something plausible. That is not a model failure; it is the schema doing exactly what it says.
| Symptom | Schema cause | Fix |
|---|---|---|
| Field missing entirely | Not in required | Add it to required |
| Invented status or category | Free string with no enum | Constrain with enum |
Number arrives as "42" | type omitted or string | Declare "type": "integer" |
| Extra unexpected keys | additionalProperties unset | Set it to false |
| Date in three formats | No format or pattern | Add "format": "date" and say so in the description |
Enable strict schema adherence where the provider offers it. On OpenAI that is "strict": true on the function definition, which makes the API enforce the schema during decoding rather than hoping the model complies. The property descriptions are also read by the model, so write them as instructions rather than as documentation.
Cause 3: you parsed a stream too early
When streaming, tool arguments arrive as deltas across many chunks. Any single chunk is meaningless on its own. You must accumulate the fragments by tool-call index and parse once the stream completes.
from collections import defaultdict
buffers = defaultdict(str)
for chunk in stream:
for call in (chunk.choices[0].delta.tool_calls or []):
buffers[call.index] += (call.function.arguments or "")
# parse only after the stream has ended
tools = {i: json.loads(b) for i, b in buffers.items()}If the connection itself drops mid-stream you have a different problem: an incomplete buffer that will never be completed. See streaming connection closed mid-response.
Handle the failure without crashing
Even with a strict schema, treat parsing as fallible. The robust pattern is to validate against your schema and, on failure, hand the error back to the model as a tool result so it can correct itself.
try:
args = Arguments.model_validate_json(raw) # pydantic
except ValidationError as e:
messages.append({
"role": "tool",
"tool_call_id": call_id,
"content": f"Invalid arguments: {e}. Re-issue the call matching the schema.",
})
# one retry, then give up rather than loop
Cap that correction loop at one or two attempts. A model that cannot satisfy the schema twice will not satisfy it on the fifth try, and each attempt costs a full round trip.
Related errors
The non-tool version of this problem is covered in the model returned invalid JSON. For the full decision table on which failures to retry, see the LLM API error reference.
Frequently asked questions
Why are tool arguments a string rather than an object?
On OpenAI, function arguments come back as serialised JSON inside tool_calls function arguments and you are expected to parse them yourself. Anthropic returns a tool_use block whose input is already an object, but it can still be incomplete if generation stopped early.
How do I stop the model inventing or omitting fields?
Constrain the schema rather than the prompt. Mark fields as required, use enum for closed sets, declare types explicitly, and set additionalProperties to false. Where the provider offers strict schema adherence, enable it so the shape is enforced during decoding instead of merely requested.
Should I retry when arguments fail validation?
Once, or at most twice, by returning the validation error to the model as a tool result so it can correct itself. A model that cannot satisfy the schema twice will not satisfy it on the fifth attempt, and every try costs a full round trip.



