OpenAI strict mode: `prefixItems` (tuple fields) is marked strict-compatible but is rejected; `minLength`/`maxLength` are marked incompatible but are accepted

Author: percymcnCreated Aug 9, 2026Updated Sep 18, 2026
Labelsbugopenaipydanty:bugserializationp:2-highattention-escalated

Summary

OpenAIJsonSchemaTransformer treats prefixItems as a valid way to type an array's elements, so any model with a tuple field is auto-marked strict-compatible and sent with strict: true. prefixItems is one of the keywords OpenAI's own SDK lists as unsupported in strict mode, so this is a request the API cannot accept.

Separately, minLength/maxLength are listed as strict-incompatible when OpenAI actually preserves them, which silently downgrades models that use Field(min_length=...) to non-strict.

The second issue masks the first, which is probably why this has survived several passes over this code (#4474, #4475, #4478, #4479, #4756, #6547): a model that has both a tuple and a min_length field lands on strict=False and never fails. Only a model with a tuple and no length-constrained string reaches the bad request.

Measured against pydantic-ai==2.27.0 and openai==7.4.0.

1. prefixItemsstrict: true → rejected by OpenAI

profiles/openai.py:570:

python
has_typed_items = isinstance(items, dict) and any(key in items for key in _TYPE_BEARING_KEYS)
if not has_typed_items and not schema.get('prefixItems'):

and the comment above it:

OpenAI strict mode requires an array to describe its elements' type, via either items (list types) or prefixItems (tuple types).

prefixItems does not satisfy strict mode — it is rejected outright. In [email protected], lib/transform.js:52 defines:

javascript
const JSON_SCHEMA_UNSUPPORTED_SCHEMA_KEYWORDS = new Set([
  ..., 'not', 'patternProperties', 'prefixItems', 'propertyNames', ...
]);

Repro:

python
from pydantic import BaseModel
from pydantic_ai import Agent

class WithTuple(BaseModel):
    title: str
    bbox: tuple[int, int, int, int]

Agent('openai:gpt-4o', output_type=WithTuple).run_sync('go')

Captured request (patched httpx.Client.send, dummy key):

json
{"name": "final_result", "strict": true, "parameters": {
  "properties": {"title": {"type": "string"},
    "bbox": {"type": "array", "maxItems": 4, "minItems": 4,
             "prefixItems": [{"type":"integer"},{"type":"integer"},{"type":"integer"},{"type":"integer"}]}},
  "required": ["title","bbox"], "type": "object", "additionalProperties": false}}

Feeding exactly those parameters to the vendor's own transformer:

toStrictJsonSchema(params)
-> Error: Schema at `properties/bbox` uses unsupported keyword `prefixItems`
   and cannot be represented in strict Structured Outputs

Suggested fix. A homogeneous tuple converts losslessly — prefixItems: [T, T, T, T] with minItems == maxItems == 4 is exactly items: T plus minItems/maxItems, both of which strict mode supports. That keeps tuple[int,int,int,int] working and strict. A heterogeneous tuple has no strict representation, so it should set is_strict_compatible = False rather than be rewritten (dropping prefixItems for a bare items would silently widen the schema). Note that popping prefixItems is required either way — leaving it beside items trades one rejection for another.

2. minLength / maxLength are not strict-incompatible

profiles/openai.py:397 lists both in _STRICT_INCOMPATIBLE_KEYS. Running each of those twelve keys through toStrictJsonSchema:

key OpenAI's own transformer
minLength preserved byte-identical
maxLength preserved byte-identical
the other 10 throws — correctly listed

So class M(BaseModel): level: str = Field(min_length=2) is sent with strict: false even though OpenAI accepts it unchanged. The cost is silent: the user still gets a 200, but the response is no longer grammar-constrained, so the schema becomes a suggestion rather than a guarantee — and nothing in the API response says so.

(pattern is already handled correctly — only lookaround patterns are rejected.)

3. Smaller: keys in OpenAI's unsupported set that aren't in _STRICT_INCOMPATIBLE_KEYS

Comparing the two lists, 17 of OpenAI's unsupported keywords are absent from yours; prefixItems above is the only one an ordinary Pydantic model emits. The rest — allOf, not, if/then/else, dependentRequired, dependentSchemas, dependencies, contentEncoding, contentMediaType, contentSchema, $anchor, $dynamicAnchor, $dynamicRef, $recursiveAnchor, $recursiveRef — are reachable when a caller supplies a raw JSON Schema for a tool rather than a Pydantic model.

I checked the obvious Pydantic routes to these and they're all handled correctly today, so this is low priority rather than a live bug: a nested model with Field(description=...) emits $ref + sibling (moved into anyOf correctly), and Base64Bytes emits format: "base64" (correctly flagged via _STRICT_COMPATIBLE_STRING_FORMATS).

Testing without an API key

toStrictJsonSchema from openai/lib/transform is the function the OpenAI SDK itself uses to build strict payloads, and it throws on exactly the unsupported set — so it works as an offline oracle. Asserting that every schema pydantic-ai marks is_strict_compatible = True survives it would catch all three of the above in CI, with no network and no key.