Five Pydantic errors you will see, and what causes each
When you validate an LLM's JSON reply with Pydantic V2, almost every failure is one of five error types: missing, extra_forbidden, int_type or int_parsing, json_invalid and value_error. I produced each one on purpose with Pydantic 2.12.5 and read the error type, then sent a Pydantic-generated schema to the Gemini API to see what a provider does with it. Two things in the older advice were wrong: V2 does accept the string "42" for an int by default, and Optional[str] without a default is a required field.
json_invalidis not a schema problem: it means the reply was cut off, wrapped in a Markdown fence, or had a trailing comma.missingon an optional field means you wroteOptional[str]without= None.- Pass the provider the right schema field: Pydantic's
model_json_schema()failed with HTTP 400 in Gemini'sresponseSchemaand worked inresponseJsonSchema. - Check the finish reason before you parse. A reply that stopped at the output limit can never validate.
What I ran
All the Pydantic checks ran offline on Python 3.10.0 with Pydantic 2.12.5 (the table below shows the error types it reported). The provider checks used the Gemini API on 9 October 2026 with gemini-3.1-flash-lite and a free-tier key. I did not test any OpenAI model: the earlier version of this guide was about one, and I have no key for it, so everything here is about Pydantic itself and about Gemini.
The errors, their causes, and the fix
| Error type | What triggered it in my test | Fix |
|---|---|---|
missing | Optional[str] and str | None with no default, key absent from the reply | Write = None, or make the model always send the key |
extra_forbidden | ConfigDict(extra="forbid") and an unexpected key in the reply | Remove forbid if you want to tolerate extra keys; the default silently drops them |
int_type | ConfigDict(strict=True) and the JSON string "42" for an int | Use the default (lax) mode for LLM output, or ask for a number in the schema |
int_parsing, int_from_float | The strings "ten" and the number 10.7 for an int | Fix the prompt or the schema type; Pydantic will not guess |
json_invalid | A reply cut off mid-string, a reply inside a Markdown fence, a trailing comma | Check the finish reason, strip the fence, raise the output limit |
value_error | A @field_validator that rejects confidence = 1.4 | Decide whether to retry, clamp or reject; the provider never ran your validator |
Type coercion: "42" is fine, "10.7" and "ten" are not
By default (lax mode) Pydantic V2 converts compatible values. For user_id: int the string "42" became 42 and "10.0" became 10, from Python and from JSON. The number 10.7 failed with int_from_float and the string "ten" with int_parsing. Only with ConfigDict(strict=True) did "42" fail (int_type), while the JSON number 42 still passed. So the advice that V2 rejects numeric strings is true only when you turn strict mode on; the default is the forgiving behavior that LLM output usually needs.
Optional is not optional without a default
In Pydantic V2, summary: Optional[str] means "a string or None is allowed", not "the key may be absent". Calling the model without the key raised missing ("Field required"), for Optional[str] and for str | None alike. Adding = None fixed it: both OptDefault() and OptDefault.model_validate_json("{}") returned summary=None. If your schema comes from a model with Optional fields and the LLM sometimes leaves a key out, this is the first thing to check.
class Analysis(BaseModel):
summary: str | None = None # key may be absent or null
confidence: float = 0.0 # key may be absent, defaults to 0.0
Truncated and wrapped replies are JSON errors, not schema errors
Pydantic reported json_invalid for every broken-JSON case I tried: a reply cut after 64, 30 or 5 characters ("EOF while parsing a string"), a reply wrapped in a Markdown fence ("expected value at line 1 column 1") and a trailing comma. In a live test I asked gemini-3.1-flash-lite for twelve items with an output limit of 60 tokens. The API answered HTTP 200 with finishReason: MAX_TOKENS, and the text ended mid-object:
...the apple is a crisp and refreshing snack."
},
{
"name":
Pydantic's verdict was json_invalid ("EOF while parsing a value"). Note that the HTTP status was 200: the failure shows up only in the finish reason. A small wrapper makes that check hard to forget. I tested it on five inputs (clean, fenced, cut off, wrong type, missing field) and it behaved as the comments say:
def parse_reply(model_cls, text, finish_reason):
"""Validate an LLM JSON reply; refuse replies that were cut off, strip a Markdown fence."""
if finish_reason not in (None, "STOP"):
raise RuntimeError(f"reply not complete (finish reason: {finish_reason})")
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[1].rsplit("```", 1)[0]
return model_cls.model_validate_json(cleaned)
Sending a Pydantic schema to a provider
A Pydantic model with a nested model, an Optional field and extra="forbid" produces this kind of schema from model_json_schema(): a $defs section with a $ref, "additionalProperties": false, and anyOf with {"type": "null"}. I sent that exact schema to Gemini in two fields:
responseSchema: HTTP 400,Unknown name "$defs" ... Cannot find field.responseJsonSchema: HTTP 200, and Pydantic validated the reply (Wrapper(owner=Profile(username='bob_smith', age=41), tags=['red', 'blue'])).
So if a provider rejects your schema, the problem is often which field you pass it in, not your Pydantic model. The keyword-by-keyword results are in my test of Gemini's schema fields. A schema accepted by the provider also does not guarantee that your validators pass: @field_validator runs only in your code, after the reply arrives.
Keeping validation errors useful
- Log the raw reply with the error.
ValidationError.errors()gives the type, the field path and a message; keep the text next to it so you can reproduce the failure. - Separate retryable from non-retryable errors.
json_invalidfrom a cut-off reply needs a larger output limit or a smaller request;value_errormay be worth one retry with the message appended to the prompt. - Keep the LLM-facing model small. A separate, flat model for the reply is easier to reason about than reusing a database model. I did not test whether this changes accuracy.
What I did not test
Any OpenAI model or its parse helper, strict structured-output modes of other providers, Pydantic 2.13 or later (the Python 3.10.0 here breaks the newest releases), and large or deeply nested schemas. Rerun the small checks above on your own Pydantic and Python versions before relying on exact error messages.
Sources and Further Reading
- Validation errors — Pydantic docs (the error types listed above)
- Conversion table — Pydantic docs (what lax and strict mode accept)
- Structured outputs — Google AI for Developers
