Troubleshooting

Pydantic V2 Validation Errors in LLM JSON Output: What Each Means, Tested

Eren Şimşir· Founder & Chief Technical Editor•
A young woman in a red polka dot shirt holds a laptop with stickers outdoors.
Photo by Anna Shvets via Pexels

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_invalid is not a schema problem: it means the reply was cut off, wrapped in a Markdown fence, or had a trailing comma.
  • missing on an optional field means you wrote Optional[str] without = None.
  • Pass the provider the right schema field: Pydantic's model_json_schema() failed with HTTP 400 in Gemini's responseSchema and worked in responseJsonSchema.
  • 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 typeWhat triggered it in my testFix
missingOptional[str] and str | None with no default, key absent from the replyWrite = None, or make the model always send the key
extra_forbiddenConfigDict(extra="forbid") and an unexpected key in the replyRemove forbid if you want to tolerate extra keys; the default silently drops them
int_typeConfigDict(strict=True) and the JSON string "42" for an intUse the default (lax) mode for LLM output, or ask for a number in the schema
int_parsing, int_from_floatThe strings "ten" and the number 10.7 for an intFix the prompt or the schema type; Pydantic will not guess
json_invalidA reply cut off mid-string, a reply inside a Markdown fence, a trailing commaCheck the finish reason, strip the fence, raise the output limit
value_errorA @field_validator that rejects confidence = 1.4Decide 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.

Pydantic V2 on an int field: lax vs. strict modeTable of five inputs for an int field. Lax mode accepts "42", "10.0" and the JSON number 42, rejects 10.7 and "ten". Strict mode rejects the string "42" and accepts the JSON number 42. Other strict-mode cases were not tested.Pydantic V2 on an int field: lax vs. strict modelax (default)strict=True"42" into an int42, okint_type"10.0" into an int10, oknot tested10.7 into an intint_from_floatnot tested"ten" into an intint_parsingnot testedJSON number 42 into anintokok
From the Pydantic 2.12.5 run described above. Only the cases the article reports are filled in; "not tested" cells were not run.

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

  1. 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.
  2. Separate retryable from non-retryable errors. json_invalid from a cut-off reply needs a larger output limit or a smaller request; value_error may be worth one retry with the message appended to the prompt.
  3. 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

Frequently Asked Questions

Does Pydantic V2 reject the string "42" for an int field?

Not by default. In lax mode, which is the default, "42" became 42 and "10.0" became 10 in my tests. Only with ConfigDict(strict=True) did "42" fail, with the error type int_type. The strings "ten" and the number 10.7 are rejected in both modes.

Why does Pydantic say "Field required" for an Optional field?

In Pydantic V2, Optional[str] and str | None only allow None as a value; they do not make the key optional. Without a default, a missing key raises the error type "missing". Write summary: str | None = None to allow the key to be absent.

Why do I get json_invalid when the API returned HTTP 200?

The reply was probably cut off by the output limit, wrapped in a Markdown fence, or had a trailing comma. In my test, a reply stopped by the limit came back with HTTP 200 and finishReason MAX_TOKENS, and Pydantic reported json_invalid. Check the finish reason before parsing.

Can I send model_json_schema() directly to the Gemini API?

It depends on the field. In my test the schema of a model with $defs, $ref and additionalProperties returned HTTP 400 in responseSchema and was accepted in responseJsonSchema. The Python SDK can also take the Pydantic class itself and convert it for you.

Should I use extra="forbid" for LLM output?

Only if you want an unexpected key to be an error. The default ignores extra keys silently, which is usually the more forgiving choice; forbid raised extra_forbidden in my test. Note that "additionalProperties": false in the generated schema is accepted by some provider fields and rejected by others.