Raw Pydantic output is richer than most strict modes accept. Providers that offer a strict or guaranteed-schema sampling mode restrict the token distribution during generation so the output conforms to the schema, which is why a schema the provider cannot compile is rejected up front rather than failing at parse time per ZeroEntropy on constrained decoding. The Microsoft and EPFL benchmark work on structured outputs notes that constrained decoding frameworks have standardized around JSON Schema, but coverage of schema features varies between frameworks per the JSONSchemaBench paper.
A community write-up of the mismatch lists the recurring offenders concretely: optional fields that Pydantic emits as a type array including null, numeric constraints such as minimum and maximum, recursive models that use references, union types expressed as anyOf or oneOf, and missing or empty additionalProperties per the LangChain forum sanitizer thread. That thread is worth reading as a checklist even if you do not use the library it targets.
The OpenAI Agents SDK encodes the same conclusion in its own type. Its function schema dataclass carries a strict_json_schema flag defaulting to true, with a comment that strict mode strongly increases the likelihood of correct JSON input, and the parameters are derived from a Pydantic model rather than hand-written per the OpenAI Agents SDK function schema reference.
A cleaning function that covers the common cases:
import copy
def to_strict(schema: dict) -> dict:
"""Rewrite a Pydantic schema into a conservative, strict-mode shape."""
s = copy.deepcopy(schema)
def walk(node):
if not isinstance(node, dict):
return node
# nullable union: {"anyOf": [X, {"type": "null"}]} -> {**X, "nullable": True}
if "anyOf" in node:
parts = [p for p in node["anyOf"] if p.get("type") != "null"]
if len(parts) == 1:
node.pop("anyOf")
node.update(walk(parts[0]))
node["nullable"] = True
node.pop("minimum", None)
node.pop("maximum", None)
node.pop("exclusiveMinimum", None)
node.pop("exclusiveMaximum", None)
if node.get("type") == "object":
node.setdefault("properties", {})
node["additionalProperties"] = False
node.setdefault("required", list(node["properties"].keys()))
for key in ("properties", "$defs"):
if key in node:
node[key] = {k: walk(v) for k, v in node[key].items()}
if "items" in node:
node["items"] = walk(node["items"])
return node
return walk(s)
Two decisions inside that function deserve a rule.
First, moving bounds such as minimum and maximum out of the schema does not erase them. Keep them in the Pydantic class, because validation still runs on the reply, and the provider's grammar just cannot enforce them during generation. The validator becomes the enforcement point for those constraints, which is fine: it is the same class.
Second, making every property required and every object closed is the strictest reading, and it changes behavior. If a field is genuinely optional, decide whether you want the model to always emit it (required plus nullable) or to sometimes omit it (optional, and your Pydantic default handles absence). Write that decision down, because it is the kind of thing that silently flips during a refactor.
| Pydantic construct | Raw JSON Schema | Problem in strict modes | Cleaning action |
|---|
| `int | None` field | anyOf with a null branch | Union forms not accepted |
Field(ge=0, le=100) | minimum, maximum | Bounds often unsupported | Strip from model-facing schema, keep in class |
| Recursive model | $ref and $defs | References sometimes rejected | Flatten or split into smaller calls |
| `str | int` | anyOf | Union forms not accepted |
| Object with no explicit close | additionalProperties absent | Validation error | Set additionalProperties false |
| Field with a default | not in required | Model may omit it | Decide required plus nullable, or accept omission |
The general design advice from the nuric post applies before any of this: if a schema is complex or deeply nested, consider dividing it into smaller, simpler schemas and calling the model more than once rather than pushing one large nested object through a grammar per the nuric post on structuring responses.