Skip to content

Commit 415f6d3

Browse files
committed
fix: convert oneOf to anyOf in strict schema for OpenAI compatibility
OpenAI's Structured Outputs API does not support oneOf in nested contexts (e.g., inside array items). Pydantic generates oneOf for discriminated unions, causing validation errors when sending schemas to OpenAI. This change modifies ensure_strict_json_schema() to convert oneOf to anyOf, which provides equivalent functionality for discriminated unions while maintaining OpenAI API compatibility. Fixes #1091
1 parent 9078e29 commit 415f6d3

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

src/agents/strict_schema.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ def _ensure_strict_json_schema(
8787
for i, variant in enumerate(any_of)
8888
]
8989

90+
# oneOf is not supported by OpenAI's structured outputs in nested contexts,
91+
# so we convert it to anyOf which provides equivalent functionality for
92+
# discriminated unions
93+
one_of = json_schema.get("oneOf")
94+
if is_list(one_of):
95+
existing_any_of = json_schema.get("anyOf", [])
96+
if not is_list(existing_any_of):
97+
existing_any_of = []
98+
json_schema["anyOf"] = existing_any_of + [
99+
_ensure_strict_json_schema(variant, path=(*path, "oneOf", str(i)), root=root)
100+
for i, variant in enumerate(one_of)
101+
]
102+
json_schema.pop("oneOf")
103+
90104
# intersections
91105
all_of = json_schema.get("allOf")
92106
if is_list(all_of):

tests/test_strict_schema_oneof.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
from typing import Annotated, Literal, Union
2+
3+
from pydantic import BaseModel, Field
4+
5+
from agents.agent_output import AgentOutputSchema
6+
from agents.strict_schema import ensure_strict_json_schema
7+
8+
9+
def test_oneof_converted_to_anyof():
10+
schema = {
11+
"type": "object",
12+
"properties": {"value": {"oneOf": [{"type": "string"}, {"type": "integer"}]}},
13+
}
14+
15+
result = ensure_strict_json_schema(schema)
16+
17+
assert "oneOf" not in str(result)
18+
assert "anyOf" in result["properties"]["value"]
19+
assert len(result["properties"]["value"]["anyOf"]) == 2
20+
21+
22+
def test_nested_oneof_in_array_items():
23+
# Test the issue #1091 scenario: oneOf in array items with discriminator
24+
schema = {
25+
"type": "object",
26+
"properties": {
27+
"steps": {
28+
"type": "array",
29+
"items": {
30+
"oneOf": [
31+
{
32+
"type": "object",
33+
"properties": {
34+
"action": {"type": "string", "const": "buy_fruit"},
35+
"color": {"type": "string"},
36+
},
37+
"required": ["action", "color"],
38+
},
39+
{
40+
"type": "object",
41+
"properties": {
42+
"action": {"type": "string", "const": "buy_food"},
43+
"price": {"type": "integer"},
44+
},
45+
"required": ["action", "price"],
46+
},
47+
],
48+
"discriminator": {
49+
"propertyName": "action",
50+
"mapping": {
51+
"buy_fruit": "#/components/schemas/BuyFruitStep",
52+
"buy_food": "#/components/schemas/BuyFoodStep",
53+
},
54+
},
55+
},
56+
}
57+
},
58+
}
59+
60+
result = ensure_strict_json_schema(schema)
61+
62+
assert "oneOf" not in str(result)
63+
items_schema = result["properties"]["steps"]["items"]
64+
assert "anyOf" in items_schema
65+
assert "discriminator" in items_schema
66+
assert items_schema["discriminator"]["propertyName"] == "action"
67+
68+
69+
def test_discriminated_union_with_pydantic():
70+
# Test with actual Pydantic models from issue #1091
71+
class FruitArgs(BaseModel):
72+
color: str
73+
74+
class FoodArgs(BaseModel):
75+
price: int
76+
77+
class BuyFruitStep(BaseModel):
78+
action: Literal["buy_fruit"]
79+
args: FruitArgs
80+
81+
class BuyFoodStep(BaseModel):
82+
action: Literal["buy_food"]
83+
args: FoodArgs
84+
85+
Step = Annotated[Union[BuyFruitStep, BuyFoodStep], Field(discriminator="action")]
86+
87+
class Actions(BaseModel):
88+
steps: list[Step]
89+
90+
output_schema = AgentOutputSchema(Actions)
91+
schema = output_schema.json_schema()
92+
93+
assert "oneOf" not in str(schema)
94+
assert "anyOf" in str(schema)
95+
96+
97+
def test_oneof_merged_with_existing_anyof():
98+
# When both anyOf and oneOf exist, they should be merged
99+
schema = {
100+
"type": "object",
101+
"anyOf": [{"type": "string"}],
102+
"oneOf": [{"type": "integer"}, {"type": "boolean"}],
103+
}
104+
105+
result = ensure_strict_json_schema(schema)
106+
107+
assert "oneOf" not in result
108+
assert "anyOf" in result
109+
assert len(result["anyOf"]) == 3
110+
111+
112+
def test_discriminator_preserved():
113+
schema = {
114+
"oneOf": [{"$ref": "#/$defs/TypeA"}, {"$ref": "#/$defs/TypeB"}],
115+
"discriminator": {
116+
"propertyName": "type",
117+
"mapping": {"a": "#/$defs/TypeA", "b": "#/$defs/TypeB"},
118+
},
119+
"$defs": {
120+
"TypeA": {
121+
"type": "object",
122+
"properties": {"type": {"const": "a"}, "value_a": {"type": "string"}},
123+
},
124+
"TypeB": {
125+
"type": "object",
126+
"properties": {"type": {"const": "b"}, "value_b": {"type": "integer"}},
127+
},
128+
},
129+
}
130+
131+
result = ensure_strict_json_schema(schema)
132+
133+
assert "discriminator" in result
134+
assert result["discriminator"]["propertyName"] == "type"
135+
assert "oneOf" not in result
136+
assert "anyOf" in result
137+
138+
139+
def test_deeply_nested_oneof():
140+
schema = {
141+
"type": "object",
142+
"properties": {
143+
"level1": {
144+
"type": "object",
145+
"properties": {
146+
"level2": {
147+
"type": "array",
148+
"items": {"oneOf": [{"type": "string"}, {"type": "number"}]},
149+
}
150+
},
151+
}
152+
},
153+
}
154+
155+
result = ensure_strict_json_schema(schema)
156+
157+
assert "oneOf" not in str(result)
158+
items = result["properties"]["level1"]["properties"]["level2"]["items"]
159+
assert "anyOf" in items
160+
161+
162+
def test_oneof_with_refs():
163+
schema = {
164+
"type": "object",
165+
"properties": {
166+
"value": {
167+
"oneOf": [{"$ref": "#/$defs/StringType"}, {"$ref": "#/$defs/IntType"}]
168+
}
169+
},
170+
"$defs": {
171+
"StringType": {"type": "string"},
172+
"IntType": {"type": "integer"},
173+
},
174+
}
175+
176+
result = ensure_strict_json_schema(schema)
177+
178+
assert "oneOf" not in str(result)
179+
assert "anyOf" in result["properties"]["value"]

0 commit comments

Comments
 (0)