From 1351700f3c57cbe44654a7311d9759c9fb1943ad Mon Sep 17 00:00:00 2001 From: foivos-all Date: Wed, 17 Jun 2026 10:11:31 -0700 Subject: [PATCH] fix(tools): resolve JSON Pointer $ref in _dereference_schema --- src/google/adk/tools/_gemini_schema_util.py | 35 ++++++++++++++++-- .../tools/test_gemini_schema_util.py | 37 +++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/google/adk/tools/_gemini_schema_util.py b/src/google/adk/tools/_gemini_schema_util.py index 6935a118b7..b19beb93cf 100644 --- a/src/google/adk/tools/_gemini_schema_util.py +++ b/src/google/adk/tools/_gemini_schema_util.py @@ -113,6 +113,27 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]: # `$defs` takes precedence on the (pathological) key collision. defs = {**schema.get("definitions", {}), **schema.get("$defs", {})} + def _resolve_json_pointer(ref_path: str, root: dict[str, Any]) -> Any: + """Resolves a JSON Pointer reference path.""" + if not ref_path.startswith("#/"): + return None + parts = ref_path[2:].split("/") + current = root + for part in parts: + part = part.replace("~1", "/").replace("~0", "~") + if isinstance(current, dict): + if part not in current: + return None + current = current[part] + elif isinstance(current, list): + try: + current = current[int(part)] + except (ValueError, IndexError): + return None + else: + return None + return current + def _resolve_refs(sub_schema: Any, path_refs: frozenset[str]) -> Any: if isinstance(sub_schema, dict): if "$ref" in sub_schema: @@ -136,9 +157,17 @@ def _resolve_refs(sub_schema: Any, path_refs: frozenset[str]) -> Any: resolved.update(sub_schema_copy) # Recursively resolve refs in the newly inserted part. return _resolve_refs(resolved, new_path) - else: - # Reference not found, return as is. - return sub_schema + + # Try to resolve as a JSON Pointer reference (e.g. #/properties/foo). + resolved = _resolve_json_pointer(ref_uri, schema) + if resolved is not None: + resolved_copy = ( + resolved.copy() if isinstance(resolved, dict) else resolved + ) + return _resolve_refs(resolved_copy, new_path) + + # Reference not found, return as is. + return sub_schema else: # No $ref, so traverse deeper into the dictionary. return { diff --git a/tests/unittests/tools/test_gemini_schema_util.py b/tests/unittests/tools/test_gemini_schema_util.py index 6aaa4ddea0..47a7c5b19b 100644 --- a/tests/unittests/tools/test_gemini_schema_util.py +++ b/tests/unittests/tools/test_gemini_schema_util.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from google.adk.tools._gemini_schema_util import _dereference_schema from google.adk.tools._gemini_schema_util import _sanitize_schema_formats_for_gemini from google.adk.tools._gemini_schema_util import _to_gemini_schema from google.adk.tools._gemini_schema_util import _to_snake_case @@ -899,6 +900,42 @@ def test_to_gemini_schema_reused_non_circular_ref(self): gemini_schema.properties["b"].properties["prop_b"].type == Type.STRING ) + def test_json_pointer_ref(self): + """Test JSON Pointer reference (#/properties/...) resolution.""" + openapi_schema = { + "type": "object", + "properties": { + "a": {"$ref": "#/properties/b"}, + "b": {"type": "string"}, + }, + } + gemini_schema = _to_gemini_schema(openapi_schema) + assert gemini_schema.properties["a"].type == Type.STRING + + def test_circular_json_pointer_ref(self): + """Test circular JSON Pointer reference.""" + openapi_schema = { + "type": "object", + "properties": { + "a": {"$ref": "#/properties/b"}, + "b": {"$ref": "#/properties/a"}, + }, + } + gemini_schema = _to_gemini_schema(openapi_schema) + assert "Circular ref" in gemini_schema.properties["a"].description + + def test_json_pointer_to_array_element(self): + """Test JSON Pointer to an array element via _dereference_schema.""" + schema = { + "type": "object", + "properties": { + "a": {"$ref": "#/properties/b/0"}, + "b": [{"type": "string"}], + }, + } + result = _dereference_schema(schema) + assert result["properties"]["a"]["type"] == "string" + class TestToSnakeCase: