Skip to content

Commit f5911d3

Browse files
conradleeclaude
andcommitted
Support enhanced JSON Schema features in Google Gemini 2.5+ models
Google announced in November 2025 that Gemini 2.5+ models now support enhanced JSON Schema features including title, $ref/$defs, anyOf/oneOf, minimum/maximum, additionalProperties, prefixItems, and property ordering. This removes workarounds in GoogleJsonSchemaTransformer and allows native $ref and oneOf support instead of forced inlining and conversion. Key findings from empirical testing: - Native $ref/$defs support confirmed (no inlining needed) - Both anyOf and oneOf work natively (no conversion needed) - exclusiveMinimum/exclusiveMaximum NOT yet supported by Google SDK Changes: - Set prefer_inlined_defs=False to use native $ref/$defs instead of inlining - Remove oneOf→anyOf conversion (both work natively now) - Remove adapter code that stripped title, additionalProperties, and prefixItems - Keep stripping exclusiveMinimum/exclusiveMaximum (not yet supported) - Remove code that raised errors for $ref schemas - Update GoogleJsonSchemaTransformer docstring to document all supported features - Update test_json_def_recursive to verify recursive schemas work with $ref - Add comprehensive test suite for new JSON Schema capabilities - Add documentation section highlighting enhanced JSON Schema support with examples 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 4cc4f35 commit f5911d3

File tree

4 files changed

+327
-64
lines changed

4 files changed

+327
-64
lines changed

docs/models/google.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,70 @@ agent = Agent(model)
201201

202202
`GoogleModel` supports multi-modal input, including documents, images, audio, and video. See the [input documentation](../input.md) for details and examples.
203203

204+
## Enhanced JSON Schema Support
205+
206+
As of November 2025, Google Gemini models (2.5+) provide enhanced support for JSON Schema features when using [`NativeOutput`](../output.md#native-output), enabling more sophisticated structured outputs:
207+
208+
### Supported Features
209+
210+
- **Property Ordering**: The order of properties in your Pydantic model definition is now preserved in the output
211+
- **Title Fields**: The `title` field is supported for providing short property descriptions
212+
- **Union Types (`anyOf` and `oneOf`)**: Full support for conditional structures using Python's `Union` or `|` type syntax
213+
- **Recursive Schemas (`$ref` and `$defs`)**: Full support for self-referential models and reusable schema definitions, enabling tree structures and recursive data
214+
- **Numeric Constraints**: `minimum` and `maximum` constraints are respected (note: `exclusiveMinimum` and `exclusiveMaximum` are not yet supported)
215+
- **Optional Fields (`type: 'null'`)**: Proper handling of optional fields with `None` values
216+
- **Additional Properties**: Dictionary fields with `dict[str, T]` are fully supported
217+
- **Tuple Types (`prefixItems`)**: Support for tuple-like array structures
218+
219+
### Example: Recursive Schema
220+
221+
```python
222+
from pydantic import BaseModel
223+
from pydantic_ai import Agent
224+
from pydantic_ai.models.google import GoogleModel
225+
from pydantic_ai.output import NativeOutput
226+
227+
class TreeNode(BaseModel):
228+
"""A tree node that can contain child nodes."""
229+
value: int
230+
children: list['TreeNode'] | None = None
231+
232+
model = GoogleModel('gemini-2.5-pro')
233+
agent = Agent(model, output_type=NativeOutput(TreeNode))
234+
235+
result = await agent.run('Create a tree with root value 1 and two children with values 2 and 3')
236+
# result.output will be a TreeNode with proper structure
237+
```
238+
239+
### Example: Union Types
240+
241+
```python
242+
from typing import Union, Literal
243+
from pydantic import BaseModel
244+
from pydantic_ai import Agent
245+
from pydantic_ai.models.google import GoogleModel
246+
from pydantic_ai.output import NativeOutput
247+
248+
class Success(BaseModel):
249+
status: Literal['success']
250+
data: str
251+
252+
class Error(BaseModel):
253+
status: Literal['error']
254+
error_message: str
255+
256+
class Response(BaseModel):
257+
result: Union[Success, Error]
258+
259+
model = GoogleModel('gemini-2.5-pro')
260+
agent = Agent(model, output_type=NativeOutput(Response))
261+
262+
result = await agent.run('Process this request successfully')
263+
# result.output.result will be either Success or Error
264+
```
265+
266+
See the [structured output documentation](../output.md) for more details on using `NativeOutput` with Pydantic models.
267+
204268
## Model settings
205269

206270
You can customize model behavior using [`GoogleModelSettings`][pydantic_ai.models.google.GoogleModelSettings]:
Lines changed: 22 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
from __future__ import annotations as _annotations
22

3-
import warnings
4-
5-
from pydantic_ai.exceptions import UserError
6-
73
from .._json_schema import JsonSchema, JsonSchemaTransformer
84
from . import ModelProfile
95

@@ -23,84 +19,55 @@ def google_model_profile(model_name: str) -> ModelProfile | None:
2319
class GoogleJsonSchemaTransformer(JsonSchemaTransformer):
2420
"""Transforms the JSON Schema from Pydantic to be suitable for Gemini.
2521
26-
Gemini which [supports](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations)
27-
a subset of OpenAPI v3.0.3.
22+
Gemini supports [a subset of OpenAPI v3.0.3](https://ai.google.dev/gemini-api/docs/function-calling#function_declarations).
23+
24+
As of November 2025, Gemini 2.5+ models support enhanced JSON Schema features including:
25+
* `title` for short property descriptions
26+
* `anyOf` and `oneOf` for conditional structures (unions)
27+
* `$ref` and `$defs` for recursive schemas and reusable definitions
28+
* `minimum` and `maximum` for numeric constraints
29+
* `additionalProperties` for dictionaries
30+
* `type: 'null'` for optional fields
31+
* `prefixItems` for tuple-like arrays
2832
29-
Specifically:
30-
* gemini doesn't allow the `title` keyword to be set
31-
* gemini doesn't allow `$defs` — we need to inline the definitions where possible
33+
Note: `exclusiveMinimum` and `exclusiveMaximum` are not yet supported by the Google SDK.
3234
"""
3335

3436
def __init__(self, schema: JsonSchema, *, strict: bool | None = None):
35-
super().__init__(schema, strict=strict, prefer_inlined_defs=True, simplify_nullable_unions=True)
37+
super().__init__(schema, strict=strict, prefer_inlined_defs=False, simplify_nullable_unions=True)
3638

3739
def transform(self, schema: JsonSchema) -> JsonSchema:
38-
# Note: we need to remove `additionalProperties: False` since it is currently mishandled by Gemini
39-
additional_properties = schema.pop(
40-
'additionalProperties', None
41-
) # don't pop yet so it's included in the warning
42-
if additional_properties:
43-
original_schema = {**schema, 'additionalProperties': additional_properties}
44-
warnings.warn(
45-
'`additionalProperties` is not supported by Gemini; it will be removed from the tool JSON schema.'
46-
f' Full schema: {self.schema}\n\n'
47-
f'Source of additionalProperties within the full schema: {original_schema}\n\n'
48-
'If this came from a field with a type like `dict[str, MyType]`, that field will always be empty.\n\n'
49-
"If Google's APIs are updated to support this properly, please create an issue on the Pydantic AI GitHub"
50-
' and we will fix this behavior.',
51-
UserWarning,
52-
)
53-
54-
schema.pop('title', None)
40+
# Remove properties not supported by Gemini
5541
schema.pop('$schema', None)
5642
if (const := schema.pop('const', None)) is not None:
5743
# Gemini doesn't support const, but it does support enum with a single value
5844
schema['enum'] = [const]
5945
schema.pop('discriminator', None)
6046
schema.pop('examples', None)
6147

62-
# TODO: Should we use the trick from pydantic_ai.models.openai._OpenAIJsonSchema
63-
# where we add notes about these properties to the field description?
64-
schema.pop('exclusiveMaximum', None)
65-
schema.pop('exclusiveMinimum', None)
66-
6748
# Gemini only supports string enums, so we need to convert any enum values to strings.
6849
# Pydantic will take care of transforming the transformed string values to the correct type.
6950
if enum := schema.get('enum'):
7051
schema['type'] = 'string'
7152
schema['enum'] = [str(val) for val in enum]
7253

7354
type_ = schema.get('type')
74-
if 'oneOf' in schema and 'type' not in schema: # pragma: no cover
75-
# This gets hit when we have a discriminated union
76-
# Gemini returns an API error in this case even though it says in its error message it shouldn't...
77-
# Changing the oneOf to an anyOf prevents the API error and I think is functionally equivalent
78-
schema['anyOf'] = schema.pop('oneOf')
79-
8055
if type_ == 'string' and (fmt := schema.pop('format', None)):
8156
description = schema.get('description')
8257
if description:
8358
schema['description'] = f'{description} (format: {fmt})'
8459
else:
8560
schema['description'] = f'Format: {fmt}'
8661

87-
if '$ref' in schema:
88-
raise UserError(f'Recursive `$ref`s in JSON Schema are not supported by Gemini: {schema["$ref"]}')
62+
# As of November 2025, Gemini 2.5+ models now support:
63+
# - additionalProperties (for dict types)
64+
# - $ref (for recursive schemas)
65+
# - prefixItems (for tuple-like arrays)
66+
# These are no longer stripped from the schema.
8967

90-
if 'prefixItems' in schema:
91-
# prefixItems is not currently supported in Gemini, so we convert it to items for best compatibility
92-
prefix_items = schema.pop('prefixItems')
93-
items = schema.get('items')
94-
unique_items = [items] if items is not None else []
95-
for item in prefix_items:
96-
if item not in unique_items:
97-
unique_items.append(item)
98-
if len(unique_items) > 1: # pragma: no cover
99-
schema['items'] = {'anyOf': unique_items}
100-
elif len(unique_items) == 1: # pragma: no branch
101-
schema['items'] = unique_items[0]
102-
schema.setdefault('minItems', len(prefix_items))
103-
if items is None: # pragma: no branch
104-
schema.setdefault('maxItems', len(prefix_items))
68+
# Note: exclusiveMinimum/exclusiveMaximum are NOT yet supported by Google SDK,
69+
# so we still need to strip them
70+
schema.pop('exclusiveMinimum', None)
71+
schema.pop('exclusiveMaximum', None)
10572

10673
return schema

tests/models/test_gemini.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,8 @@ class Locations(BaseModel):
445445

446446

447447
async def test_json_def_recursive(allow_model_requests: None):
448+
"""Test that recursive schemas with $ref are now supported (as of November 2025)."""
449+
448450
class Location(BaseModel):
449451
lat: float
450452
lng: float
@@ -479,15 +481,18 @@ class Location(BaseModel):
479481
description='This is the tool for the final Result',
480482
parameters_json_schema=json_schema,
481483
)
482-
with pytest.raises(UserError, match=r'Recursive `\$ref`s in JSON Schema are not supported by Gemini'):
483-
mrp = ModelRequestParameters(
484-
function_tools=[],
485-
allow_text_output=True,
486-
output_tools=[output_tool],
487-
output_mode='text',
488-
output_object=None,
489-
)
490-
mrp = m.customize_request_parameters(mrp)
484+
# As of November 2025, Gemini 2.5+ models support recursive $ref in JSON Schema
485+
# This should no longer raise an error
486+
mrp = ModelRequestParameters(
487+
function_tools=[],
488+
allow_text_output=True,
489+
output_tools=[output_tool],
490+
output_mode='text',
491+
output_object=None,
492+
)
493+
mrp = m.customize_request_parameters(mrp)
494+
# Verify the schema still contains $ref after customization
495+
assert '$ref' in mrp.output_tools[0].parameters_json_schema
491496

492497

493498
async def test_json_def_date(allow_model_requests: None):

0 commit comments

Comments
 (0)