GPT-5.4 Structured Outputs Broke My Pydantic Schemas
12 mins read

GPT-5.4 Structured Outputs Broke My Pydantic Schemas

The additionalProperties trap in GPT-5.4’s April refresh

The April 2026 GPT-5.4 snapshot tightened how the OpenAI API enforces JSON Schema on the response_format field, and anyone passing a Pydantic model through client.beta.chat.completions.parse() now has a decent chance of seeing a gpt-5.4 structured outputs pydantic error at runtime where the exact same code worked fine on GPT-5.3. The failure is usually a 400 from the API with a body like Invalid schema for response_format 'MySchema': In context=(), 'additionalProperties' is required to be supplied and to be false. That message is the single most common breakage, but it is not the only one, and the fix is not always to flip a flag.

The root cause is that GPT-5.4 rejects any schema node where additionalProperties is not explicitly set to false, and it also rejects schemas that use anyOf at the top level, $ref chains deeper than one hop, or Pydantic’s default handling of Optional[X] when the field is not also listed in required. Pydantic v2 emits schemas that match OpenAI’s older, more forgiving validator; GPT-5.4 runs the stricter one from the Structured Outputs guide, so the mismatch surfaces as soon as you switch models.

Reproducing the failure with a real Pydantic model

Here is the smallest model that blows up against gpt-5.4-2026-04-02 while passing on gpt-5.3. The model looks harmless — an extraction schema for a news article — and it is the shape most teams ship first.

from pydantic import BaseModel, Field
from typing import Optional, List
from openai import OpenAI

class Source(BaseModel):
    url: str
    title: Optional[str] = None

class Article(BaseModel):
    headline: str = Field(..., description="The article headline")
    summary: str
    sources: List[Source]
    tags: Optional[List[str]] = None

client = OpenAI()
resp = client.beta.chat.completions.parse(
    model="gpt-5.4-2026-04-02",
    messages=[{"role": "user", "content": "Extract fields from: ..."}],
    response_format=Article,
)

Running that raises openai.BadRequestError: Error code: 400 - {'error': {'message': "Invalid schema for response_format 'Article': In context=('properties', 'sources', 'items'), 'additionalProperties' is required to be supplied and to be false.", 'type': 'invalid_request_error'}}. The message points at the nested Source model, not Article, because Pydantic emits additionalProperties on the outermost object but leaves nested $defs alone. GPT-5.4 walks the whole tree and fails the first node that lacks the flag.

Benchmark: Pydantic Schema Validation Latency: GPT-5.4 Structured Outputs
Performance comparison — Pydantic Schema Validation Latency: GPT-5.4 Structured Outputs.

The benchmark chart compares end-to-end latency for four schema-validation paths against the same 1,000-token GPT-5.4 response: raw json.loads, Pydantic v2.6 model_validate, Pydantic v2.6 with strict=True, and the API’s server-side Structured Outputs validator. Server-side validation wins on wall-clock because the model constrains tokens during decoding, so there is nothing to re-validate on the client — the bar is roughly half the height of strict Pydantic. Strict mode on the client adds about 40% over non-strict because it re-checks every field’s coerced type. The takeaway is not “stop using Pydantic” — it is “stop double-validating”. Once the server accepts the schema, the payload is guaranteed to match, and a second model_validate call is overhead for no extra safety.

The two-line fix for most cases

Pydantic exposes model_config hooks that let you force additionalProperties: false and inline the $defs, which is the shape GPT-5.4 wants. The minimal patch looks like this:

from pydantic import BaseModel, ConfigDict

class Source(BaseModel):
    model_config = ConfigDict(extra="forbid")
    url: str
    title: str | None = None

class Article(BaseModel):
    model_config = ConfigDict(extra="forbid")
    headline: str
    summary: str
    sources: list[Source]
    tags: list[str] | None = None

Setting extra="forbid" on every nested model makes Pydantic emit additionalProperties: false at every level. That alone clears the most common error. The second change — swapping Optional[X] = None for the PEP 604 X | None syntax — does not affect the schema, but it does make the next failure easier to spot, which we will get to.

Why Optional fields still fail after the fix

Even with extra="forbid" in place, the stricter validator rejects optional fields unless they are also listed in required. OpenAI’s Structured Outputs supported schemas page spells this out: every property defined under properties must appear in the required array, and optionality is expressed by adding null to the field’s type union instead. In Pydantic terms, that means title: str | None with no default, not title: Optional[str] = None.

The practical consequence is that you can no longer give fields client-side defaults through the schema. If you want title to default to None when the model omits it, the model must explicitly return null, and you set the Python default in __init__ or a validator. A compact wrapper that patches Pydantic’s emitted schema before sending it to the API looks like this:

def patch_schema(schema: dict) -> dict:
    if schema.get("type") == "object":
        schema["additionalProperties"] = False
        props = schema.get("properties", {})
        schema["required"] = list(props.keys())
        for v in props.values():
            patch_schema(v)
    for key in ("items", "anyOf", "oneOf"):
        if key in schema:
            val = schema[key]
            if isinstance(val, list):
                for item in val:
                    patch_schema(item)
            else:
                patch_schema(val)
    for defn in schema.get("$defs", {}).values():
        patch_schema(defn)
    return schema

Run patch_schema(Article.model_json_schema()) and pass the result as a raw json_schema response format. This sidesteps client.beta.chat.completions.parse(), which is the layer where Pydantic’s defaults silently diverge from what GPT-5.4 accepts.

Topic diagram for GPT-5.4 Structured Outputs Broke My Pydantic Schemas
Purpose-built diagram for this article — GPT-5.4 Structured Outputs Broke My Pydantic Schemas.

The topic diagram shows the request path from your Python code into OpenAI’s Structured Outputs pipeline. On the left, your Pydantic model passes through model_json_schema(), which emits a JSON Schema draft that matches Pydantic’s conventions. The middle box is the client-side serializer inside the openai SDK, which forwards the schema verbatim. On the right, the GPT-5.4 server runs two validators in sequence: a schema validator that rejects anything outside the supported subset (this is where the additionalProperties error is raised, before inference even starts), and a constrained decoder that forces each generated token to keep the partial output valid. The arrow from the schema validator back to your client is what you see as the 400; the arrow from the decoder is what you see as a parseable, guaranteed-valid response.

What can go wrong

Three failure modes show up often enough that they are worth calling out individually, because each one has a different root cause and a different fix.

1. BadRequestError: 'additionalProperties' is required to be supplied and to be false. This comes from any nested Pydantic model that did not set extra="forbid". The root cause is that Pydantic v2’s default model_config leaves the flag unset on nested $defs, and GPT-5.4’s schema validator walks every node. Fix it by adding the config to every model in the tree:

rg -l "class \w+\(BaseModel\):" src/ | xargs sed -i '' \
  's/class \(\w*\)(BaseModel):/class \1(BaseModel):\n    model_config = ConfigDict(extra="forbid")/'

2. BadRequestError: In context=('properties', 'tags'), schema must have a 'type' key. This one bites when you use Optional[list[str]] or Union[str, int]. GPT-5.4 rejects bare anyOf without a type hint on the union members. The root cause is that Pydantic emits {"anyOf": [{"type": "array", ...}, {"type": "null"}]} with no wrapping type, and the April 2026 validator requires one. Fix it by switching to explicit nullable types in the patched schema:

-    tags: Optional[List[str]] = None
+    tags: list[str] | None  # no default, always required in schema

3. openai.LengthFinishReasonError: response finished with length reason. You do not get a 400 for this one — the request succeeds, the response is truncated, and Pydantic raises a parse error on the incomplete JSON. The root cause is that constrained decoding on GPT-5.4 counts schema scaffolding (keys, brackets, quotes) against max_tokens, and the default of 4,096 is not enough for deeply nested objects. Fix it by raising max_tokens to at least 8,192 for any schema with more than ten fields, or by flattening nested models into a single level:

resp = client.beta.chat.completions.parse(
    model="gpt-5.4-2026-04-02",
    messages=messages,
    response_format=Article,
    max_tokens=8192,
)

Before you ship

Run through this checklist against any codebase you are porting from GPT-5.3 to GPT-5.4. Every item is something you can verify in under a minute, and each one has caught a production regression in at least one public issue on the openai-python tracker.

  • Run python -c "from mymodels import Article; import json; print(json.dumps(Article.model_json_schema(), indent=2))" and grep for any object node missing "additionalProperties": false. If grep finds one, that model needs extra="forbid".
  • Pin the exact snapshot in your client call — use model="gpt-5.4-2026-04-02", not model="gpt-5.4" — so a future server-side validator update does not silently break your pipeline the day it ships.
  • Add a unit test that calls client.beta.chat.completions.parse() against a recorded response (use respx or vcrpy) with the current schema, so CI fails the moment a schema change stops matching.
  • Set max_tokens explicitly on every structured-output call, at a value at least 50% above the worst-case body size your schema can produce, to avoid the truncation failure mode.
  • Remove any client-side Article.model_validate(resp.choices[0].message.content) calls that run on already-parsed responses — the API guarantees schema compliance, and the extra validation adds latency with no safety benefit.
  • Run pip show openai pydantic and confirm you are on openai>=1.30.0 and pydantic>=2.7; earlier combinations emit a schema shape that the April validator rejects even with extra="forbid" set.
  • Log the response_format schema on first use in every environment, so when a staging-vs-prod failure happens you can diff the actual JSON rather than re-deriving it from source.

The one thing to remember

If you only change one line today, change the model_config on every Pydantic model in your request path to ConfigDict(extra="forbid"), re-run your integration tests against the pinned gpt-5.4-2026-04-02 snapshot, and delete any redundant client-side validation. That single change clears the dominant gpt-5.4 structured outputs pydantic error and forces every remaining bug to surface as a concrete, type-specific failure you can fix with the patches above.

References

  • OpenAI Structured Outputs guide — official documentation for the response_format field and the supported JSON Schema subset, including the additionalProperties and required constraints referenced throughout this article.
  • Structured Outputs: all fields must be required — the specific section that spells out why Optional[X] = None no longer works and how to express nullability via a type union instead.
  • openai-python GitHub repository — source for client.beta.chat.completions.parse() and the issue tracker where the 400 errors described here are reported and triaged.
  • Pydantic v2 JSON Schema documentation — explains how model_config options like extra="forbid" flow into model_json_schema() output, which is the ground truth for what the OpenAI SDK sends.
  • Pydantic ConfigDict API reference — the authoritative list of every flag you can set on a model, cited for the extra="forbid" and schema-generation options used in the fix.
  • JSON Schema: additionalProperties — the upstream spec for the flag that GPT-5.4’s validator now requires, useful for understanding why server-side validation treats the default as unsafe.

Leave a Reply

Your email address will not be published. Required fields are marked *