> ## Documentation Index
> Fetch the complete documentation index at: https://docs.r28.ai/charter/llms.txt
> Use this file to discover all available pages before exploring further.

# LLM input auto-corrections

> The three input mistakes models make repeatedly, and how the runtime repairs them.

The schema a model is handed is generated by the contract, not the one you wrote
— so it can accept forms the wire never will. Three mistakes models make
reliably are absorbed at the boundary instead of coming back as a failed call.

There is a fourth mistake that is deliberately *not* absorbed, and it is the
last section on this page: an argument the schema does not declare is refused
rather than dropped.

***

## `'{"requests": [...]}'` becomes `{"requests": [...]}`

Models sometimes serialize a nested object as a JSON *string* instead of passing
it as an object. A `model_validator(mode="before")` finds any
`BaseModel`/`list`/`dict`-typed field whose value is a string starting with `{`
or `[`, and parses it before field validation runs.

***

## `"dateTime"` becomes `date_time`

Models send camelCase keys inside nested objects, matching the API's own docs. A
schema that answered only to its own field names would drop the key as unknown:
the field stays `None`, the API receives `{}`, and returns a confusing 400 with
no local validation error at all.

So the contract answers to all three spellings. Every field carries camelCase,
PascalCase and snake\_case aliases — `dateTime`, `DateTime` and `date_time` are
all valid input — and this holds whichever convention the schema's author wrote
in. Stripe's schemas are snake\_case and Gmail's are camelCase; both resolve in
both directions.

All three are *declared* aliases rather than merely tolerated ones, which is
what makes the refusal in the next section safe: with unknown keys refused, a
spelling that is only "also allowed" would become an error.

`model_dump()` is unaffected — it still returns snake\_case field names, so the
key-case conversion at serialization time works exactly as before.

***

## An argument the schema does not declare is refused

Pydantic's default is to ignore unknown keys. At a tool boundary that is the
worst available behaviour, because the result is not an error — it is a wrong
answer that nothing marks as wrong.

A model that invents a filter, or mistypes a real one, previously got a
successful call and an unfiltered page:

```python theme={null}
# `created` is not a field on this tool.
await stripe.customers_list.ainvoke({"limit": 2, "created": {"gt": 1704067200}})
```

That sent `limit=2`, returned two arbitrary customers, and said nothing
anywhere about the filter it had thrown away. The agent then reasons over rows
it believes were filtered. The same happens for a plausible typo — `emails` for
`email` — which is the more likely mistake of the two.

So generated schemas set `extra="forbid"`, and the call above raises
[`ToolValidationError`](/charter/charter/reference/errors#toolvalidationerror) before the
network:

```
Validation error:
- **created**: Extra inputs are not permitted
```

which is a message a model can read and correct, in the
[retry loop](/charter/charter/running/tool-validation-error-handling) this library already has.
`to_json_schema()` carries `additionalProperties: false` for the same reason, on
the root and on every nested object — so a provider enforcing strict schemas
mostly prevents the mistake before a call is made at all.

<Warning>
  This tightened in a way a host application can notice. If you were passing an
  extra key to `ainvoke` and relying on it being ignored, that call now raises.
  The fix is to remove the key, or to declare it on the schema if it should have
  been on the wire. Per-call `headers` are unaffected: they are a keyword-only
  argument, not part of the schema, and never were.
</Warning>

***

## A rejection the model can act on

Left to the agent framework, a rejected call comes back wrapped in that
framework's own template, with the entire kwargs payload dumped at the model:

```
Error invoking tool 'documents_update' with kwargs {'document_id': '...', 'body': '...'}
with error: body: Input should be a valid dictionary ... Please fix the error and try again.
```

Charter raises [`ToolValidationError`](/charter/charter/reference/errors#toolvalidationerror)
carrying a compact rendering instead:

```
Validation error:
- **body**: Input should be a valid dictionary or instance of BatchUpdateRequestBody_LLM (got 12345)
```

The structured per-field errors are on the exception too (`.errors`), so a host
application can render its own version.

***

## Malformed JSON says *why*, not just *what*

When the JSON-string coercion above fails, the naive behaviour is to pass the
string through and let type checking complain:

```
Validation error:
- **body**: Input should be a valid dictionary or instance of BatchUpdateRequestBody_LLM
```

That is a *type* error. The model concludes it sent the wrong structure, retries
with different content and the same broken syntax, and loops forever — because
the real problem was a syntax error, such as one extra closing brace.

Charter raises with the decoder's own detail:

```
Validation error:
- **(input)**: 'body' looks like JSON but is malformed — fix the syntax and retry.
  Detail: Expecting ',' delimiter: line 1 column 105
```

Now the model knows it is a syntax problem and where. The check is unambiguous: a
string starting with `{` or `[` for a `BaseModel`/`list`/`dict`-typed field is
always an attempted JSON serialization — there is no other case.

## Related

* [`ToolValidationError`](/charter/charter/reference/errors#toolvalidationerror) — the `.errors` list these renderings come from
* [Tool validation and error handling](/charter/charter/running/tool-validation-error-handling) — the three layers, in order
