Skip to content

strict_mode: non-null default values are left in the schema and rejected on Azure #4390

Description

@antoniojose-perez

ensure_strict_json_schema() only strips the default key from a property when its value is exactly None:

# strip `None` defaults as there's no meaningful distinction here
# the schema will still be `nullable` and the model will default
# to using `None` anyway
if json_schema.get("default", NOT_GIVEN) is None:
    json_schema.pop("default")

Any field with a non-null default (e.g. an enum, a Decimal, an int, a plain string) keeps its default key in the generated strict schema. OpenAI's Structured Outputs API rejects that schema at call time with:

'default' is not permitted within a property definition

This forces users to disable strict_mode entirely for any tool whose Pydantic model has a field with a non-null default, losing the schema guarantees strict mode is meant to provide.

Repro

from decimal import Decimal
from enum import Enum
from pydantic import BaseModel
from agents import function_tool

class Currency(str, Enum):
    EUR = "EUR"
    USD = "USD"

class Invoice(BaseModel):
    total: Decimal
    currency: Currency = Currency.EUR  # non-null default

@function_tool
def create_invoice(data: Invoice):
    ...

# create_invoice.params_json_schema still contains
# {"currency": {..., "default": "EUR"}}
# -> OpenAI API call fails with:
# "'default' is not permitted within a property definition"

Expected behavior

default should be stripped from every property in the strict schema, not just when it's None.

Why this is safe to fix unconditionally

A few lines above in the same function, every property is already forced into required:

json_schema["required"] = list(properties.keys())

That's how strict mode simulates an "optional" field — it stays required, but its type allows null. Once a field is required, the model must always return an explicit value, so the Python-level default is never actually used by the API. It's dead weight in a strict schema regardless of whether its value is None or something else — the current is None check is an arbitrary distinction that shouldn't exist.

Suggested fix

# strip ALL defaults, not just `None`: since every property is marked
# `required` above, the model must always supply an explicit value for
# the field regardless of any Python-level default — so `default` is
# dead weight in a strict schema, and the API rejects it outright when
# non-null.
json_schema.pop("default", None)

(The NOT_GIVEN import becomes unused after this change if nothing else in the file relies on it.)

if json_schema.get("default", NOT_GIVEN) is None:

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions