Skip to content

Commit 3164cb2

Browse files
authored
Raise error when StructuredDict is used with recursive JSON schema refs (#3050)
1 parent c3a7058 commit 3164cb2

File tree

4 files changed

+46
-4
lines changed

4 files changed

+46
-4
lines changed

pydantic_ai_slim/pydantic_ai/_json_schema.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ def walk(self) -> JsonSchema:
5454
if not self.prefer_inlined_defs and self.defs:
5555
handled['$defs'] = {k: self._handle(v) for k, v in self.defs.items()}
5656

57-
elif self.recursive_refs: # pragma: no cover
57+
elif self.recursive_refs:
5858
# If we are preferring inlined defs and there are recursive refs, we _have_ to use a $defs+$ref structure
5959
# We try to use whatever the original root key was, but if it is already in use,
6060
# we modify it to avoid collisions.
6161
defs = {key: self.defs[key] for key in self.recursive_refs}
6262
root_ref = self.schema.get('$ref')
6363
root_key = None if root_ref is None else re.sub(r'^#/\$defs/', '', root_ref)
64-
if root_key is None:
64+
if root_key is None: # pragma: no cover
6565
root_key = self.schema.get('title', 'root')
6666
while root_key in defs:
6767
# Modify the root key until it is not already in use
@@ -77,6 +77,8 @@ def _handle(self, schema: JsonSchema) -> JsonSchema:
7777
if self.prefer_inlined_defs:
7878
while ref := schema.get('$ref'):
7979
key = re.sub(r'^#/\$defs/', '', ref)
80+
if key in self.recursive_refs:
81+
break
8082
if key in self.refs_stack:
8183
self.recursive_refs.add(key)
8284
break # recursive ref can't be unpacked

pydantic_ai_slim/pydantic_ai/output.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic_core import core_schema
1010
from typing_extensions import TypeAliasType, TypeVar, deprecated
1111

12-
from . import _utils
12+
from . import _utils, exceptions
1313
from ._json_schema import InlineDefsJsonSchemaTransformer
1414
from .messages import ToolCallPart
1515
from .tools import DeferredToolRequests, ObjectJsonSchema, RunContext, ToolDefinition
@@ -316,6 +316,10 @@ def StructuredDict(
316316
# See https://github.com/pydantic/pydantic/issues/12145
317317
if '$defs' in json_schema:
318318
json_schema = InlineDefsJsonSchemaTransformer(json_schema).walk()
319+
if '$defs' in json_schema:
320+
raise exceptions.UserError(
321+
'`StructuredDict` does not currently support recursive `$ref`s and `$defs`. See https://github.com/pydantic/pydantic/issues/12145 for more information.'
322+
)
319323

320324
if name:
321325
json_schema['title'] = name

pydantic_evals/pydantic_evals/generation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ async def generate_dataset(
5959
"""
6060
output_schema = dataset_type.model_json_schema_with_evaluators(custom_evaluator_types)
6161

62-
# TODO(DavidM): Update this once we add better response_format and/or ResultTool support to Pydantic AI
62+
# TODO: Use `output_type=StructuredDict(output_schema)` (and `from_dict` below) once https://github.com/pydantic/pydantic/issues/12145
63+
# is fixed and `StructuredDict` no longer needs to use `InlineDefsJsonSchemaTransformer`.
6364
agent = Agent(
6465
model,
6566
system_prompt=(

tests/test_agent.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,41 @@ def call_tool(_: list[ModelMessage], info: AgentInfo) -> ModelResponse:
14661466
assert result.output == snapshot({'make': 'Toyota', 'model': 'Camry', 'tires': [{'brand': 'Michelin', 'size': 17}]})
14671467

14681468

1469+
def test_structured_dict_recursive_refs():
1470+
class Node(BaseModel):
1471+
nodes: list['Node'] | dict[str, 'Node']
1472+
1473+
schema = Node.model_json_schema()
1474+
assert schema == snapshot(
1475+
{
1476+
'$defs': {
1477+
'Node': {
1478+
'properties': {
1479+
'nodes': {
1480+
'anyOf': [
1481+
{'items': {'$ref': '#/$defs/Node'}, 'type': 'array'},
1482+
{'additionalProperties': {'$ref': '#/$defs/Node'}, 'type': 'object'},
1483+
],
1484+
'title': 'Nodes',
1485+
}
1486+
},
1487+
'required': ['nodes'],
1488+
'title': 'Node',
1489+
'type': 'object',
1490+
}
1491+
},
1492+
'$ref': '#/$defs/Node',
1493+
}
1494+
)
1495+
with pytest.raises(
1496+
UserError,
1497+
match=re.escape(
1498+
'`StructuredDict` does not currently support recursive `$ref`s and `$defs`. See https://github.com/pydantic/pydantic/issues/12145 for more information.'
1499+
),
1500+
):
1501+
StructuredDict(schema)
1502+
1503+
14691504
def test_default_structured_output_mode():
14701505
def hello(_: list[ModelMessage], _info: AgentInfo) -> ModelResponse:
14711506
return ModelResponse(parts=[TextPart(content='hello')]) # pragma: no cover

0 commit comments

Comments
 (0)