Skip to content

Commit 476e914

Browse files
conradleeclaude
andcommitted
Fix nested Pydantic models in Gemini by removing title from $defs (fixes #3483)
## Problem Gemini's function calling parser incorrectly interprets 'title' fields in nested JSON Schema definitions ($defs) as callable Python functions, causing MALFORMED_FUNCTION_CALL errors when using nested Pydantic models. ## Root Cause When a schema like this is sent to Gemini: ```json { "$defs": { "MiddleModel": { "title": "MiddleModel", // ← Gemini treats this as a callable "type": "object" } } } ``` Gemini tries to generate Python constructor calls like: `MiddleModel(title="...", items=[...])` Instead of JSON: `{"title": "...", "items": [...]}` ## Solution Remove 'title' from nested schemas (those in $defs) while preserving: - $ref/$defs structure (better schema organization) - Top-level title (needed for function declaration name) This is simpler than the original approach of inlining all $ref definitions. ## Testing - Added test_google_nested_models_without_native_output (reproduces issue) - Added test_google_nested_models_with_native_output (workaround verification) - All 81 Google model tests pass - Verified with real Gemini API calls ## Related Issues - Fixes #3483 - Reported to Google SDK: googleapis/python-genai#1732 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent fd1569e commit 476e914

File tree

2 files changed

+49
-56
lines changed

2 files changed

+49
-56
lines changed

pydantic_ai_slim/pydantic_ai/profiles/google.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,10 @@ class GoogleJsonSchemaTransformer(JsonSchemaTransformer):
3939
4040
Note: Gemini's tool calling system treats 'title' fields in nested schemas as callable function names,
4141
causing MALFORMED_FUNCTION_CALL errors. This fixes issue #3483 where nested Pydantic models were
42-
treated as tool calls instead of structured output schema. We inline $ref definitions in tool schemas
43-
so we can traverse and remove problematic titles.
42+
treated as tool calls instead of structured output schema. We remove 'title' from nested schemas
43+
(those defined in $defs) while preserving $ref/$defs structure for better schema organization.
4444
"""
4545

46-
def __init__(
47-
self,
48-
schema: JsonSchema,
49-
*,
50-
strict: bool | None = None,
51-
prefer_inlined_defs: bool = True, # Inline by default for tools
52-
simplify_nullable_unions: bool = False,
53-
):
54-
super().__init__(
55-
schema,
56-
strict=strict,
57-
prefer_inlined_defs=prefer_inlined_defs,
58-
simplify_nullable_unions=simplify_nullable_unions,
59-
)
60-
6146
def transform(self, schema: JsonSchema) -> JsonSchema:
6247
# Remove properties not supported by Gemini
6348
schema.pop('$schema', None)
@@ -70,8 +55,11 @@ def transform(self, schema: JsonSchema) -> JsonSchema:
7055
# Remove 'title' from nested schemas - Gemini treats these as callable function names
7156
# in tool calling mode, causing MALFORMED_FUNCTION_CALL errors for nested objects.
7257
# Only keep title at the root level for the function declaration name.
73-
if self.refs_stack: # We're inside a nested schema
74-
schema.pop('title', None)
58+
# We detect nested schemas by checking if the title matches a key in $defs.
59+
if self.defs and 'title' in schema:
60+
# Check if this schema's title matches any key in $defs (meaning it's a nested definition)
61+
if schema.get('title') in self.defs:
62+
schema.pop('title', None)
7563

7664
type_ = schema.get('type')
7765
if type_ == 'string' and (fmt := schema.pop('format', None)):

tests/models/cassettes/test_google/test_google_nested_models_without_native_output.yaml

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interactions:
88
connection:
99
- keep-alive
1010
content-length:
11-
- '1272'
11+
- '1379'
1212
content-type:
1313
- application/json
1414
host:
@@ -36,38 +36,43 @@ interactions:
3636
- description: Represents the top-level structure.
3737
name: final_result
3838
parameters_json_schema:
39+
$defs:
40+
MiddleModel:
41+
description: Represents the middle nested level.
42+
properties:
43+
items:
44+
description: List of nested items
45+
items:
46+
$ref: '#/$defs/NestedModel'
47+
type: array
48+
title:
49+
description: Title of the page
50+
type: string
51+
required:
52+
- title
53+
- items
54+
type: object
55+
NestedModel:
56+
description: Represents the deepest nested level.
57+
properties:
58+
name:
59+
description: Name of the item
60+
type: string
61+
value:
62+
description: Value of the item
63+
type: integer
64+
required:
65+
- name
66+
- value
67+
type: object
3968
properties:
4069
name:
4170
description: Name of the collection
4271
type: string
4372
pages:
4473
description: List of pages
4574
items:
46-
description: Represents the middle nested level.
47-
properties:
48-
items:
49-
description: List of nested items
50-
items:
51-
description: Represents the deepest nested level.
52-
properties:
53-
name:
54-
description: Name of the item
55-
type: string
56-
value:
57-
description: Value of the item
58-
type: integer
59-
required:
60-
- name
61-
- value
62-
type: object
63-
type: array
64-
title:
65-
description: Title of the page
66-
type: string
67-
required:
68-
- title
69-
- items
70-
type: object
75+
$ref: '#/$defs/MiddleModel'
7176
type: array
7277
required:
7378
- name
@@ -80,11 +85,11 @@ interactions:
8085
alt-svc:
8186
- h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
8287
content-length:
83-
- '3071'
88+
- '2943'
8489
content-type:
8590
- application/json; charset=UTF-8
8691
server-timing:
87-
- gfet4t7; dur=2143
92+
- gfet4t7; dur=2257
8893
transfer-encoding:
8994
- chunked
9095
vary:
@@ -100,33 +105,33 @@ interactions:
100105
name: Example Collection
101106
pages:
102107
- items:
103-
- name: Item A1
108+
- name: Item A
104109
value: 1
105-
- name: Item B1
110+
- name: Item B
106111
value: 2
107112
title: Page 1
108113
- items:
109-
- name: Item A2
114+
- name: Item C
110115
value: 3
111-
- name: Item B2
116+
- name: Item D
112117
value: 4
113118
title: Page 2
114119
name: final_result
115-
thoughtSignature: CsgIAdHtim8VFy7oQIecPD1ll2aPBug66ePPEq/2lmU5jqIzT6swtvzKGTsYES6M9HsO1eYgGSpEzQn6A/oB99vXW8L/jRzSFQ81oGtrZZ72sUHyzIwlRkwZEzWg3hsT2j1oIhmsY0WuIUO0frXHmfOQ/Oj2qUv9uoWwqr/gQ8gsPW+Z/CHdsOMLYVED3ZvXAIbquFV5+ejYfui+Ng6tITlm47eDGkd3FwpAF2SLtO0ZPZaCoqoGyjNMrRGSK++VJV+0v7UFpOOgjkf4/9jmbD9VHkSiJ++XY87Kcjj96S35COhI48Boss8H/Amu+PPop+onKgUfit6+Mmoh2fq5LFoRDCprflbIi1TPulVRopby2z6PoaStQwz53dnO732y7girFMNJ4JHTKD0NwZXo74rAAejeMeZ4CvmPju0iXgI57CAQe1X5S9p2Y6pTL++JadoRtkK4ZdKOs1AWIKhptsmy5avi3qJr/xNGqdYyoJCQQ9BjwjrR2d8qZyPhV6nSIxBXY5aw8RmsaWr5FwNPRrzi420TvEPe9Ssp5uR3xk1+dusKGpi1HCHSQIyNLZ8NybPK002RU1CgomoRx+0EG5lVvKxvUGbFCaf4A4Y+qYQOd+X0HyDCLCBUJYjKOJAautjejS7qoTc1mKAaS1ZHWsnU5C7uUGbB4nkudXIX1J4LNkdlUnhCfhlJFEvKFcbI8otmRoKth4K2/+gcSSbELmKUzGqhUOECqpyrAdz0bx/7FM5pgB+s4FuaWGRXHsevhZowbteC4Q0XunbgvrujYpTfghzR8Y+KgztCO4bjtmI1Uz9X+faRJlNu5W7DZEIsRjuJACGrfQIMHKIoAFTIs/HmP3gzmyYPn/GpN/5KZ8yCWQ5FveyehI34mbRXCWPqNbV7OVy3eIUI5MtGkd59dFGeXGFF1LpCCWVtAKrYxyPqrS2SgZqmyU7pZo1Y0QXhXyCilhpKhs7bHvpBIUMY/byUAubYHFt44GfyL/4/tkUIrcSVteoJ2Wjc1rjaljNYOhLouk50xQq8iuXfQWZWufn+aWEtQo9mSul7/+2+qcW5A6zCYy8FpVZnTBz2cxKm/vRHXhSVT0b4rP0YGzGyBOHwNDQx1Gx8RPpqqiEFOS8RRhD+0mj3PhbSni4EtXbAZqQYRv286x75Mh53PuJNzEg46GC9fgVIeiBz8J9Ep6dl3/d4NmwksYHfYlDX/Jl1N9zwWgxjaYIhtug3F5gAzsxoOwlDWxi2UWy+5zzzu7eLfGwcxPx+gQ70K/yOxMdS86SPILoM7tCcgw6FjzlGytQGwY7zQdFdrDpHHKMGVLicfjZetV1NPZX9lYWwSUk7MfHr7Rg6VzUVqq1FWo2o84hjdrAdf2aejj4tNdAMfMZe20mz1QTmdiG5TLPURWMPGRbFDnsaOuV3VkbomB5BJzac8zAAyLtogKsxOcppn1zUzcTc0JmyYed92g==
120+
thoughtSignature: Cu0HAdHtim/Us0P5TwBWE7yYzcsEdZQ1lCnTajHuXXzYUQcnrY+KXPclwqZ8upv3qmjNgTs8ahVMRiGHhHxGJ0qSjCtlo2HepWcH270aSkdkuIh2krevTqiKPXG7u5g30Jl/pvyHDKq6xkcxfzLfglyhArnCFbh16y89vfGiWC+K2QYjL23p/rjntkxCCZ3TMmsrudi/98HFOYu48hXuoG6YpFJ9C+ibgsxEhcffoEFaeQs4UqzfHhcz/dSr/wSinaA7QpRFPczmDRHphB8R7rrSSA5pV7ymRnkpE0SORjOlpUQwFDD5jFlXgTGy1CQt/FUYnXAPyRJawbaEbsc9HCs6J81hZZPOdxZN8tR5E7+nGpJJR0xmT02L60B9IXGXIiFb0QpUymk6E0L6jOVQBIHN5PaNrZC25YOrWqOGNLqaIukgnnF7nsqMP3mb/qOwUyKgcZ0O89OMcI6V9fSPz/df2m+PQvoNOM0qNtMKrVR0XDYqaZdhmK6Sv1uEWiQxS4mqra+f+SrQjskytlmxAAIoEeGx0jQC1x+avU68LtllwvXk6pJ3PddP0b3osNLokKxuv1VLfach+bNECqTmDD/LdLY/JMEof2hZSagqtDDfRbxBQB2WK2QPDs37Pg/LU4xXpj/FkTSs/Bxu0/GHylxY1/m0eR6RpAdq4XDctajLL3Y5IWLEQCJkSBtq67+InVpFFxQXAKx+WtAWshjTJzgkRbHxIVonrpG5aV2dysoQ73+GPkJfcXFGsY3aduc+xaViMfaqLuzh0IbtEQ2r+NWSePEfhmPOqztIwQuxgscEmHER6D1NdnwO8sv2GyHvxSzq5mjBtLg7bghK3UwLX2SPZ/COB71hcD4sYQ88RHv5OTAmI+0nKEHCD6fiAsRmCRspwLaAElkdCSIUPl8grfu9W9MP9w/eFW2tkcVW37TDO2dpY+Kn+qN8pPoDB0L2mcHUBt+TBL2xbgsiFnul1REQcZALB7tYbjMyLPqKx/ci1Eo6xAv/YOzX8Zbv4VNdjuT40X7+zXFrsgj0tXpga9/ymmZgynbVPUz0Ll+BAFXMmoVrqnVrEjCPxLuZFUFov7+DY88f8m1/ZWdNncj3Je74Oes7Aw5Ms/ynsY7VnzcnbyKOyLrT+Eg9CkB0COHvEMrOsJNceebAoESym/eCDtlDRrvcM9CCzV4NVH+2nvlEXYrEts6SV5iaztophBU74/dNWzAy9DlnihfIrYxRNTCgOJMXwSquViyKeeQk9eH61QvK2MF052bA68VkBGfgne0FHtG4n0bvpLO73KTH8aGcrvb0zIIpu13pT7GFACQDl5cR/R/y9sWDuoyh6LBG
116121
role: model
117122
finishMessage: Model generated function call(s).
118123
finishReason: STOP
119124
index: 0
120125
modelVersion: gemini-2.5-flash
121-
responseId: GNEeadfuJfyAkdUPs_LP8A8
126+
responseId: X94eaezvBLO1vdIP6brwoQc
122127
usageMetadata:
123-
candidatesTokenCount: 97
128+
candidatesTokenCount: 93
124129
promptTokenCount: 219
125130
promptTokensDetails:
126131
- modality: TEXT
127132
tokenCount: 219
128-
thoughtsTokenCount: 331
129-
totalTokenCount: 647
133+
thoughtsTokenCount: 323
134+
totalTokenCount: 635
130135
status:
131136
code: 200
132137
message: OK

0 commit comments

Comments
 (0)