Skip to content

Commit 7de5bc5

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: Fix RecursionError in ADK framework by adding circular reference detection to schema resolution
PiperOrigin-RevId: 905576545
1 parent 2d61cb6 commit 7de5bc5

2 files changed

Lines changed: 128 additions & 6 deletions

File tree

src/google/adk/tools/_gemini_schema_util.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,20 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]:
108108

109109
defs = schema.get("$defs", {})
110110

111-
def _resolve_refs(sub_schema: Any) -> Any:
111+
def _resolve_refs(sub_schema: Any, path_refs: frozenset[str]) -> Any:
112112
if isinstance(sub_schema, dict):
113113
if "$ref" in sub_schema:
114-
ref_key = sub_schema["$ref"].split("/")[-1]
114+
ref_uri = sub_schema["$ref"]
115+
ref_key = ref_uri.split("/")[-1]
116+
117+
if ref_uri in path_refs:
118+
return {
119+
"type": "object",
120+
"description": f"Circular ref to {ref_key}",
121+
}
122+
123+
new_path = path_refs | {ref_uri}
124+
115125
if ref_key in defs:
116126
# Found the reference, replace it with the definition.
117127
resolved = defs[ref_key].copy()
@@ -120,21 +130,24 @@ def _resolve_refs(sub_schema: Any) -> Any:
120130
del sub_schema_copy["$ref"]
121131
resolved.update(sub_schema_copy)
122132
# Recursively resolve refs in the newly inserted part.
123-
return _resolve_refs(resolved)
133+
return _resolve_refs(resolved, new_path)
124134
else:
125135
# Reference not found, return as is.
126136
return sub_schema
127137
else:
128138
# No $ref, so traverse deeper into the dictionary.
129-
return {key: _resolve_refs(value) for key, value in sub_schema.items()}
139+
return {
140+
key: _resolve_refs(value, path_refs)
141+
for key, value in sub_schema.items()
142+
}
130143
elif isinstance(sub_schema, list):
131144
# Traverse into lists.
132-
return [_resolve_refs(item) for item in sub_schema]
145+
return [_resolve_refs(item, path_refs) for item in sub_schema]
133146
else:
134147
# Not a dict or list, return as is.
135148
return sub_schema
136149

137-
dereferenced_schema = _resolve_refs(schema)
150+
dereferenced_schema = _resolve_refs(schema, frozenset())
138151
# Remove the definitions block after resolving.
139152
if "$defs" in dereferenced_schema:
140153
del dereferenced_schema["$defs"]

tests/unittests/tools/test_gemini_schema_util.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,115 @@ def test_to_gemini_schema_boolean_true_in_array_items_properties(self):
730730
model_schema = data_schema.items.properties["model"]
731731
assert model_schema.type == Type.OBJECT
732732

733+
def test_to_gemini_schema_circular_ref(self):
734+
"""Test that circular references in schema are handled without RecursionError."""
735+
openapi_schema = {
736+
"$defs": {
737+
"Node": {
738+
"type": "object",
739+
"properties": {
740+
"name": {"type": "string"},
741+
"parent": {"$ref": "#/$defs/Node"},
742+
},
743+
}
744+
},
745+
"properties": {"tree": {"$ref": "#/$defs/Node"}},
746+
"type": "object",
747+
}
748+
# Should not raise RecursionError
749+
gemini_schema = _to_gemini_schema(openapi_schema)
750+
assert gemini_schema.type == Type.OBJECT
751+
assert gemini_schema.properties["tree"].type == Type.OBJECT
752+
assert (
753+
gemini_schema.properties["tree"].properties["name"].type == Type.STRING
754+
)
755+
assert (
756+
gemini_schema.properties["tree"].properties["parent"].type
757+
== Type.OBJECT
758+
), "The circular ref should be handled and return the fallback object"
759+
assert (
760+
gemini_schema.properties["tree"].properties["parent"].description
761+
== "Circular ref to Node"
762+
)
763+
764+
def test_to_gemini_schema_multi_step_circular_ref(self):
765+
"""Test that multi-step circular references (Value -> Struct -> Value) are handled."""
766+
openapi_schema = {
767+
"$defs": {
768+
"Value": {
769+
"anyOf": [
770+
{"type": "string"},
771+
{"$ref": "#/$defs/Struct"},
772+
]
773+
},
774+
"Struct": {
775+
"type": "object",
776+
"properties": {
777+
"fields": {
778+
"type": "object",
779+
"properties": {
780+
"my_val": {
781+
"type": "array",
782+
"items": {"$ref": "#/$defs/Value"},
783+
}
784+
},
785+
}
786+
},
787+
},
788+
},
789+
"properties": {"root": {"$ref": "#/$defs/Value"}},
790+
"type": "object",
791+
}
792+
793+
gemini_schema = _to_gemini_schema(openapi_schema)
794+
# Individual assertions are used here instead of comparing the whole Schema
795+
# object or its properties dictionary because Schema objects with deep
796+
# nesting can have subtle differences in default fields that are hard to
797+
# debug due to pytest truncation limits.
798+
assert gemini_schema.type == Type.OBJECT
799+
# root is Value, which resolved to anyOf
800+
assert len(gemini_schema.properties["root"].any_of) == 2
801+
assert gemini_schema.properties["root"].any_of[0].type == Type.STRING
802+
# any_of[1] is Struct
803+
struct_schema = gemini_schema.properties["root"].any_of[1]
804+
assert struct_schema.type == Type.OBJECT
805+
assert struct_schema.properties["fields"].type == Type.OBJECT
806+
# properties["fields"].properties["my_val"] is an array
807+
my_val_schema = struct_schema.properties["fields"].properties["my_val"]
808+
assert my_val_schema.type == Type.ARRAY
809+
assert (
810+
my_val_schema.items.type == Type.OBJECT
811+
), "Array items referencing a circular $ref should resolve to Type.OBJECT"
812+
813+
def test_to_gemini_schema_reused_non_circular_ref(self):
814+
"""Test that reused non-circular references are handled correctly."""
815+
openapi_schema = {
816+
"$defs": {
817+
"CommonType": {"type": "string"},
818+
"ObjectA": {
819+
"type": "object",
820+
"properties": {"prop_a": {"$ref": "#/$defs/CommonType"}},
821+
},
822+
"ObjectB": {
823+
"type": "object",
824+
"properties": {"prop_b": {"$ref": "#/$defs/CommonType"}},
825+
},
826+
},
827+
"properties": {
828+
"a": {"$ref": "#/$defs/ObjectA"},
829+
"b": {"$ref": "#/$defs/ObjectB"},
830+
},
831+
"type": "object",
832+
}
833+
gemini_schema = _to_gemini_schema(openapi_schema)
834+
assert gemini_schema.type == Type.OBJECT
835+
assert (
836+
gemini_schema.properties["a"].properties["prop_a"].type == Type.STRING
837+
)
838+
assert (
839+
gemini_schema.properties["b"].properties["prop_b"].type == Type.STRING
840+
)
841+
733842

734843
class TestToSnakeCase:
735844

0 commit comments

Comments
 (0)