From a17a8d2ed73470df64582b5654a10cfc622668e3 Mon Sep 17 00:00:00 2001 From: foivos-all Date: Tue, 9 Dec 2025 10:07:47 -0800 Subject: [PATCH 1/5] fix: Detect and handle circular `$ref` in `_dereference_schema` --- src/google/adk/tools/_gemini_schema_util.py | 67 ++++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/src/google/adk/tools/_gemini_schema_util.py b/src/google/adk/tools/_gemini_schema_util.py index d2ed560ef1..6e1c552b80 100644 --- a/src/google/adk/tools/_gemini_schema_util.py +++ b/src/google/adk/tools/_gemini_schema_util.py @@ -78,11 +78,48 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]: """Resolves $ref pointers in a JSON schema.""" defs = schema.get("$defs", {}) + # Track references currently being resolved to detect circular dependencies. + resolving = set() + + def _resolve_json_pointer(ref_path: str, root: dict) -> Any: + """Resolves a JSON Pointer reference path.""" + if not ref_path.startswith("#/"): + return None + + # Split the path into parts, skipping the leading "#/". + parts = ref_path[2:].split("/") + current = root + + # Traverse the schema following the path. + for part in parts: + if isinstance(current, dict): + current = current.get(part) + else: + return None + if current is None: + return None + + return current - def _resolve_refs(sub_schema: Any) -> Any: + def _resolve_refs(sub_schema: Any, path: str = "#") -> Any: if isinstance(sub_schema, dict): if "$ref" in sub_schema: - ref_key = sub_schema["$ref"].split("/")[-1] + ref = sub_schema["$ref"] + + # Detect circular references by checking if we're already resolving + # this reference in the current call stack. + if ref in resolving: + # Return a placeholder schema to break the cycle. + return { + "type": "object", + "description": f"Circular reference to {ref}" + } + + # Mark this reference as being resolved. + resolving.add(ref) + + # Try to resolve as a $defs-style reference first. + ref_key = ref.split("/")[-1] if ref_key in defs: # Found the reference, replace it with the definition. resolved = defs[ref_key].copy() @@ -91,16 +128,30 @@ def _resolve_refs(sub_schema: Any) -> Any: del sub_schema_copy["$ref"] resolved.update(sub_schema_copy) # Recursively resolve refs in the newly inserted part. - return _resolve_refs(resolved) - else: - # Reference not found, return as is. - return sub_schema + result = _resolve_refs(resolved, ref) + # Done resolving this reference, remove from tracking set. + resolving.discard(ref) + return result + + # Try to resolve as a JSON Pointer reference. + resolved = _resolve_json_pointer(ref, schema) + if resolved is not None: + # Copy the resolved schema to avoid modifying the original. + resolved_copy = resolved.copy() if isinstance(resolved, dict) else resolved + # Recursively resolve refs in the resolved schema. + result = _resolve_refs(resolved_copy, ref) + resolving.discard(ref) + return result + + # Reference not found in either $defs or as a JSON Pointer, return as is. + resolving.discard(ref) + return sub_schema else: # No $ref, so traverse deeper into the dictionary. - return {key: _resolve_refs(value) for key, value in sub_schema.items()} + return {key: _resolve_refs(value, f"{path}/{key}") for key, value in sub_schema.items()} elif isinstance(sub_schema, list): # Traverse into lists. - return [_resolve_refs(item) for item in sub_schema] + return [_resolve_refs(item, f"{path}[{i}]") for i, item in enumerate(sub_schema)] else: # Not a dict or list, return as is. return sub_schema From 81c75c9d1044194f101d7f5bf9692440afb6e337 Mon Sep 17 00:00:00 2001 From: Fivos Allagiotis <74879779+foivos-all@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:17:28 -0800 Subject: [PATCH 2/5] Update src/google/adk/tools/_gemini_schema_util.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/tools/_gemini_schema_util.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/google/adk/tools/_gemini_schema_util.py b/src/google/adk/tools/_gemini_schema_util.py index 6e1c552b80..789f30c90c 100644 --- a/src/google/adk/tools/_gemini_schema_util.py +++ b/src/google/adk/tools/_gemini_schema_util.py @@ -92,10 +92,9 @@ def _resolve_json_pointer(ref_path: str, root: dict) -> Any: # Traverse the schema following the path. for part in parts: - if isinstance(current, dict): - current = current.get(part) - else: + if not isinstance(current, dict): return None + current = current.get(part) if current is None: return None From d10441249a5b817208e9787c4f3cb7386557c84e Mon Sep 17 00:00:00 2001 From: foivos-all Date: Thu, 11 Dec 2025 16:31:41 -0800 Subject: [PATCH 3/5] fix: executed `pyink` --- src/google/adk/tools/_gemini_schema_util.py | 36 +++++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/google/adk/tools/_gemini_schema_util.py b/src/google/adk/tools/_gemini_schema_util.py index 789f30c90c..07800872a6 100644 --- a/src/google/adk/tools/_gemini_schema_util.py +++ b/src/google/adk/tools/_gemini_schema_util.py @@ -80,16 +80,16 @@ def _dereference_schema(schema: dict[str, Any]) -> dict[str, Any]: defs = schema.get("$defs", {}) # Track references currently being resolved to detect circular dependencies. resolving = set() - + def _resolve_json_pointer(ref_path: str, root: dict) -> Any: """Resolves a JSON Pointer reference path.""" if not ref_path.startswith("#/"): return None - + # Split the path into parts, skipping the leading "#/". parts = ref_path[2:].split("/") current = root - + # Traverse the schema following the path. for part in parts: if not isinstance(current, dict): @@ -97,26 +97,26 @@ def _resolve_json_pointer(ref_path: str, root: dict) -> Any: current = current.get(part) if current is None: return None - + return current def _resolve_refs(sub_schema: Any, path: str = "#") -> Any: if isinstance(sub_schema, dict): if "$ref" in sub_schema: ref = sub_schema["$ref"] - + # Detect circular references by checking if we're already resolving # this reference in the current call stack. if ref in resolving: # Return a placeholder schema to break the cycle. return { - "type": "object", - "description": f"Circular reference to {ref}" + "type": "object", + "description": f"Circular reference to {ref}", } - + # Mark this reference as being resolved. resolving.add(ref) - + # Try to resolve as a $defs-style reference first. ref_key = ref.split("/")[-1] if ref_key in defs: @@ -131,26 +131,34 @@ def _resolve_refs(sub_schema: Any, path: str = "#") -> Any: # Done resolving this reference, remove from tracking set. resolving.discard(ref) return result - + # Try to resolve as a JSON Pointer reference. resolved = _resolve_json_pointer(ref, schema) if resolved is not None: # Copy the resolved schema to avoid modifying the original. - resolved_copy = resolved.copy() if isinstance(resolved, dict) else resolved + resolved_copy = ( + resolved.copy() if isinstance(resolved, dict) else resolved + ) # Recursively resolve refs in the resolved schema. result = _resolve_refs(resolved_copy, ref) resolving.discard(ref) return result - + # Reference not found in either $defs or as a JSON Pointer, return as is. resolving.discard(ref) return sub_schema else: # No $ref, so traverse deeper into the dictionary. - return {key: _resolve_refs(value, f"{path}/{key}") for key, value in sub_schema.items()} + return { + key: _resolve_refs(value, f"{path}/{key}") + for key, value in sub_schema.items() + } elif isinstance(sub_schema, list): # Traverse into lists. - return [_resolve_refs(item, f"{path}[{i}]") for i, item in enumerate(sub_schema)] + return [ + _resolve_refs(item, f"{path}[{i}]") + for i, item in enumerate(sub_schema) + ] else: # Not a dict or list, return as is. return sub_schema From 628b3006915d66d8422a9338b755da6692cee9b8 Mon Sep 17 00:00:00 2001 From: foivos-all Date: Fri, 23 Jan 2026 23:32:20 -0800 Subject: [PATCH 4/5] test: add _dereference_schema circular $ref tests --- tests/unittests/tools/test_circular_refs.py | 424 ++++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 tests/unittests/tools/test_circular_refs.py diff --git a/tests/unittests/tools/test_circular_refs.py b/tests/unittests/tools/test_circular_refs.py new file mode 100644 index 0000000000..475eca49f3 --- /dev/null +++ b/tests/unittests/tools/test_circular_refs.py @@ -0,0 +1,424 @@ +from google.adk.tools._gemini_schema_util import _dereference_schema + + +class TestDereferenceSchemaCircularRefs: + """Test circular $ref detection and handling in _dereference_schema.""" + + def test_simple_circular_ref(self): + """Test detection of simple circular reference (linked list).""" + schema = { + "$defs": { + "Node": { + "type": "object", + "properties": { + "value": {"type": "string"}, + "next": {"$ref": "#/$defs/Node"} + } + } + }, + "$ref": "#/$defs/Node" + } + + # Should not raise RecursionError + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_nested_circular_ref_binary_tree(self): + """Test circular reference in binary tree structure.""" + schema = { + "$defs": { + "TreeNode": { + "type": "object", + "properties": { + "value": {"type": "integer"}, + "left": {"$ref": "#/$defs/TreeNode"}, + "right": {"$ref": "#/$defs/TreeNode"} + } + } + }, + "type": "object", + "properties": { + "root": {"$ref": "#/$defs/TreeNode"} + } + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "root" in result["properties"] + + def test_mutual_circular_refs(self): + """Test mutually recursive circular references.""" + schema = { + "$defs": { + "Person": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "friends": { + "type": "array", + "items": {"$ref": "#/$defs/Person"} + }, + "spouse": {"$ref": "#/$defs/Person"} + } + } + }, + "$ref": "#/$defs/Person" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_deep_circular_ref_chain(self): + """Test circular reference through multiple definition levels (A→B→C→A).""" + schema = { + "$defs": { + "A": { + "type": "object", + "properties": { + "b": {"$ref": "#/$defs/B"} + } + }, + "B": { + "type": "object", + "properties": { + "c": {"$ref": "#/$defs/C"} + } + }, + "C": { + "type": "object", + "properties": { + "a": {"$ref": "#/$defs/A"} + } + } + }, + "$ref": "#/$defs/A" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_non_circular_refs_still_dereference(self): + """Test that non-circular refs are properly dereferenced.""" + schema = { + "$defs": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"} + } + }, + "Person": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "address": {"$ref": "#/$defs/Address"} + } + } + }, + "$ref": "#/$defs/Person" + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "address" in result["properties"] + # Non-circular refs should be fully dereferenced + assert "properties" in result["properties"]["address"] + + def test_array_with_circular_items(self): + """Test circular reference in array items (category tree).""" + schema = { + "$defs": { + "Category": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "subcategories": { + "type": "array", + "items": {"$ref": "#/$defs/Category"} + } + } + } + }, + "$ref": "#/$defs/Category" + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_mixed_circular_and_non_circular(self): + """Test schema with both circular and non-circular references.""" + schema = { + "$defs": { + "Metadata": { + "type": "object", + "properties": { + "created": {"type": "string"} + } + }, + "Document": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "metadata": {"$ref": "#/$defs/Metadata"}, + "parent": {"$ref": "#/$defs/Document"} + } + } + }, + "$ref": "#/$defs/Document" + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "metadata" in result["properties"] + + def test_circular_ref_with_anyof(self): + """Test circular reference inside anyOf composition.""" + schema = { + "$defs": { + "Node": { + "type": "object", + "properties": { + "value": {"type": "string"}, + "child": { + "anyOf": [ + {"$ref": "#/$defs/Node"}, + {"type": "null"} + ] + } + } + } + }, + "$ref": "#/$defs/Node" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_with_allof(self): + """Test circular reference inside allOf composition.""" + schema = { + "$defs": { + "Base": { + "type": "object", + "properties": { + "id": {"type": "integer"} + } + }, + "Extended": { + "allOf": [ + {"$ref": "#/$defs/Base"}, + { + "type": "object", + "properties": { + "parent": {"$ref": "#/$defs/Extended"} + } + } + ] + } + }, + "$ref": "#/$defs/Extended" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_with_oneof(self): + """Test circular reference inside oneOf composition.""" + schema = { + "$defs": { + "PolymorphicNode": { + "type": "object", + "properties": { + "data": {"type": "string"}, + "next": { + "oneOf": [ + {"$ref": "#/$defs/PolymorphicNode"}, + {"type": "string"}, + {"type": "null"} + ] + } + } + } + }, + "$ref": "#/$defs/PolymorphicNode" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_multiple_independent_circular_refs(self): + """Test schema with multiple independent circular structures.""" + schema = { + "$defs": { + "Tree": { + "type": "object", + "properties": { + "value": {"type": "integer"}, + "children": { + "type": "array", + "items": {"$ref": "#/$defs/Tree"} + } + } + }, + "Graph": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "neighbors": { + "type": "array", + "items": {"$ref": "#/$defs/Graph"} + } + } + } + }, + "type": "object", + "properties": { + "tree": {"$ref": "#/$defs/Tree"}, + "graph": {"$ref": "#/$defs/Graph"} + } + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_empty_schema(self): + """Test empty schema doesn't cause issues.""" + schema = {} + result = _dereference_schema(schema) + assert result == {} + + def test_schema_without_refs(self): + """Test schema without any $ref works normally.""" + schema = { + "type": "object", + "properties": { + "name": {"type": "string"}, + "age": {"type": "integer"} + } + } + result = _dereference_schema(schema) + assert result == schema + + def test_invalid_ref_graceful_handling(self): + """Test graceful handling of invalid $ref.""" + schema = { + "$defs": { + "ValidType": { + "type": "object", + "properties": { + "field": {"type": "string"} + } + } + }, + "$ref": "#/$defs/NonExistentType" + } + # Should handle gracefully without crashing + result = _dereference_schema(schema) + assert result is not None + + def test_deeply_nested_non_circular_refs(self): + """Test deeply nested but non-circular reference chain.""" + schema = { + "$defs": { + "Level1": { + "type": "object", + "properties": { + "level2": {"$ref": "#/$defs/Level2"} + } + }, + "Level2": { + "type": "object", + "properties": { + "level3": {"$ref": "#/$defs/Level3"} + } + }, + "Level3": { + "type": "object", + "properties": { + "value": {"type": "string"} + } + } + }, + "$ref": "#/$defs/Level1" + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_circular_ref_with_additional_properties(self): + """Test circular reference in additionalProperties.""" + schema = { + "$defs": { + "DynamicNode": { + "type": "object", + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": {"$ref": "#/$defs/DynamicNode"} + } + }, + "$ref": "#/$defs/DynamicNode" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_self_referencing_with_nullable(self): + """Test self-reference with nullable field (optional next pointer).""" + schema = { + "$defs": { + "LinkedList": { + "type": "object", + "properties": { + "data": {"type": "string"}, + "next": { + "anyOf": [ + {"$ref": "#/$defs/LinkedList"}, + {"type": "null"} + ] + } + } + } + }, + "$ref": "#/$defs/LinkedList" + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_reproducing_issue_3870(self): + """Reproduce the exact scenario from issue #3870.""" + # This is the type of schema that would cause RecursionError before the fix + schema = { + "$defs": { + "RecursiveType": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "children": { + "type": "array", + "items": {"$ref": "#/$defs/RecursiveType"} + } + } + } + }, + "type": "object", + "properties": { + "root": {"$ref": "#/$defs/RecursiveType"} + } + } + + # Before the fix, this would raise RecursionError + # After the fix, it should complete successfully + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "root" in result["properties"] From 487c5576ac2bb40a14f747e53f61162192c6d0da Mon Sep 17 00:00:00 2001 From: foivos-all Date: Mon, 26 Jan 2026 11:52:11 -0800 Subject: [PATCH 5/5] test: fix formatting --- tests/unittests/tools/test_circular_refs.py | 757 +++++++++----------- 1 file changed, 358 insertions(+), 399 deletions(-) diff --git a/tests/unittests/tools/test_circular_refs.py b/tests/unittests/tools/test_circular_refs.py index 475eca49f3..14fe986a45 100644 --- a/tests/unittests/tools/test_circular_refs.py +++ b/tests/unittests/tools/test_circular_refs.py @@ -2,423 +2,382 @@ class TestDereferenceSchemaCircularRefs: - """Test circular $ref detection and handling in _dereference_schema.""" - - def test_simple_circular_ref(self): - """Test detection of simple circular reference (linked list).""" - schema = { - "$defs": { - "Node": { - "type": "object", - "properties": { - "value": {"type": "string"}, - "next": {"$ref": "#/$defs/Node"} - } - } - }, - "$ref": "#/$defs/Node" - } - - # Should not raise RecursionError - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - - def test_nested_circular_ref_binary_tree(self): - """Test circular reference in binary tree structure.""" - schema = { - "$defs": { - "TreeNode": { - "type": "object", - "properties": { - "value": {"type": "integer"}, - "left": {"$ref": "#/$defs/TreeNode"}, - "right": {"$ref": "#/$defs/TreeNode"} - } - } - }, - "type": "object", - "properties": { - "root": {"$ref": "#/$defs/TreeNode"} + """Test circular $ref detection and handling in _dereference_schema.""" + + def test_simple_circular_ref(self): + """Test detection of simple circular reference (linked list).""" + schema = { + "$defs": { + "Node": { + "type": "object", + "properties": { + "value": {"type": "string"}, + "next": {"$ref": "#/$defs/Node"}, + }, } - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - assert "root" in result["properties"] - - def test_mutual_circular_refs(self): - """Test mutually recursive circular references.""" - schema = { - "$defs": { - "Person": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "friends": { - "type": "array", - "items": {"$ref": "#/$defs/Person"} - }, - "spouse": {"$ref": "#/$defs/Person"} - } - } - }, - "$ref": "#/$defs/Person" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_deep_circular_ref_chain(self): - """Test circular reference through multiple definition levels (A→B→C→A).""" - schema = { - "$defs": { - "A": { - "type": "object", - "properties": { - "b": {"$ref": "#/$defs/B"} - } + }, + "$ref": "#/$defs/Node", + } + + # Should not raise RecursionError + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_nested_circular_ref_binary_tree(self): + """Test circular reference in binary tree structure.""" + schema = { + "$defs": { + "TreeNode": { + "type": "object", + "properties": { + "value": {"type": "integer"}, + "left": {"$ref": "#/$defs/TreeNode"}, + "right": {"$ref": "#/$defs/TreeNode"}, }, - "B": { - "type": "object", - "properties": { - "c": {"$ref": "#/$defs/C"} - } + } + }, + "type": "object", + "properties": {"root": {"$ref": "#/$defs/TreeNode"}}, + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "root" in result["properties"] + + def test_mutual_circular_refs(self): + """Test mutually recursive circular references.""" + schema = { + "$defs": { + "Person": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "friends": { + "type": "array", + "items": {"$ref": "#/$defs/Person"}, + }, + "spouse": {"$ref": "#/$defs/Person"}, }, - "C": { - "type": "object", - "properties": { - "a": {"$ref": "#/$defs/A"} - } - } - }, - "$ref": "#/$defs/A" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_non_circular_refs_still_dereference(self): - """Test that non-circular refs are properly dereferenced.""" - schema = { - "$defs": { - "Address": { - "type": "object", - "properties": { - "street": {"type": "string"}, - "city": {"type": "string"} - } + } + }, + "$ref": "#/$defs/Person", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_deep_circular_ref_chain(self): + """Test circular reference through multiple definition levels (A→B→C→A).""" + schema = { + "$defs": { + "A": {"type": "object", "properties": {"b": {"$ref": "#/$defs/B"}}}, + "B": {"type": "object", "properties": {"c": {"$ref": "#/$defs/C"}}}, + "C": {"type": "object", "properties": {"a": {"$ref": "#/$defs/A"}}}, + }, + "$ref": "#/$defs/A", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_non_circular_refs_still_dereference(self): + """Test that non-circular refs are properly dereferenced.""" + schema = { + "$defs": { + "Address": { + "type": "object", + "properties": { + "street": {"type": "string"}, + "city": {"type": "string"}, }, - "Person": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "address": {"$ref": "#/$defs/Address"} - } - } }, - "$ref": "#/$defs/Person" - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - assert "address" in result["properties"] - # Non-circular refs should be fully dereferenced - assert "properties" in result["properties"]["address"] - - def test_array_with_circular_items(self): - """Test circular reference in array items (category tree).""" - schema = { - "$defs": { - "Category": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "subcategories": { - "type": "array", - "items": {"$ref": "#/$defs/Category"} - } - } - } - }, - "$ref": "#/$defs/Category" - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - - def test_mixed_circular_and_non_circular(self): - """Test schema with both circular and non-circular references.""" - schema = { - "$defs": { - "Metadata": { - "type": "object", - "properties": { - "created": {"type": "string"} - } + "Person": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "address": {"$ref": "#/$defs/Address"}, }, - "Document": { - "type": "object", - "properties": { - "title": {"type": "string"}, - "metadata": {"$ref": "#/$defs/Metadata"}, - "parent": {"$ref": "#/$defs/Document"} - } - } - }, - "$ref": "#/$defs/Document" - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - assert "metadata" in result["properties"] - - def test_circular_ref_with_anyof(self): - """Test circular reference inside anyOf composition.""" - schema = { - "$defs": { - "Node": { - "type": "object", - "properties": { - "value": {"type": "string"}, - "child": { - "anyOf": [ - {"$ref": "#/$defs/Node"}, - {"type": "null"} - ] - } - } - } }, - "$ref": "#/$defs/Node" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_circular_ref_with_allof(self): - """Test circular reference inside allOf composition.""" - schema = { - "$defs": { - "Base": { - "type": "object", - "properties": { - "id": {"type": "integer"} - } + }, + "$ref": "#/$defs/Person", + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "address" in result["properties"] + # Non-circular refs should be fully dereferenced + assert "properties" in result["properties"]["address"] + + def test_array_with_circular_items(self): + """Test circular reference in array items (category tree).""" + schema = { + "$defs": { + "Category": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "subcategories": { + "type": "array", + "items": {"$ref": "#/$defs/Category"}, + }, }, - "Extended": { - "allOf": [ - {"$ref": "#/$defs/Base"}, - { - "type": "object", - "properties": { - "parent": {"$ref": "#/$defs/Extended"} - } - } - ] - } - }, - "$ref": "#/$defs/Extended" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_circular_ref_with_oneof(self): - """Test circular reference inside oneOf composition.""" - schema = { - "$defs": { - "PolymorphicNode": { - "type": "object", - "properties": { - "data": {"type": "string"}, - "next": { - "oneOf": [ - {"$ref": "#/$defs/PolymorphicNode"}, - {"type": "string"}, - {"type": "null"} - ] - } - } - } + } + }, + "$ref": "#/$defs/Category", + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_mixed_circular_and_non_circular(self): + """Test schema with both circular and non-circular references.""" + schema = { + "$defs": { + "Metadata": { + "type": "object", + "properties": {"created": {"type": "string"}}, }, - "$ref": "#/$defs/PolymorphicNode" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_multiple_independent_circular_refs(self): - """Test schema with multiple independent circular structures.""" - schema = { - "$defs": { - "Tree": { - "type": "object", - "properties": { - "value": {"type": "integer"}, - "children": { - "type": "array", - "items": {"$ref": "#/$defs/Tree"} - } - } + "Document": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "metadata": {"$ref": "#/$defs/Metadata"}, + "parent": {"$ref": "#/$defs/Document"}, }, - "Graph": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "neighbors": { - "type": "array", - "items": {"$ref": "#/$defs/Graph"} - } - } - } }, - "type": "object", - "properties": { - "tree": {"$ref": "#/$defs/Tree"}, - "graph": {"$ref": "#/$defs/Graph"} - } - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - - def test_empty_schema(self): - """Test empty schema doesn't cause issues.""" - schema = {} - result = _dereference_schema(schema) - assert result == {} - - def test_schema_without_refs(self): - """Test schema without any $ref works normally.""" - schema = { - "type": "object", - "properties": { - "name": {"type": "string"}, - "age": {"type": "integer"} + }, + "$ref": "#/$defs/Document", + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "metadata" in result["properties"] + + def test_circular_ref_with_anyof(self): + """Test circular reference inside anyOf composition.""" + schema = { + "$defs": { + "Node": { + "type": "object", + "properties": { + "value": {"type": "string"}, + "child": { + "anyOf": [{"$ref": "#/$defs/Node"}, {"type": "null"}] + }, + }, } - } - result = _dereference_schema(schema) - assert result == schema - - def test_invalid_ref_graceful_handling(self): - """Test graceful handling of invalid $ref.""" - schema = { - "$defs": { - "ValidType": { - "type": "object", - "properties": { - "field": {"type": "string"} - } - } + }, + "$ref": "#/$defs/Node", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_with_allof(self): + """Test circular reference inside allOf composition.""" + schema = { + "$defs": { + "Base": { + "type": "object", + "properties": {"id": {"type": "integer"}}, + }, + "Extended": { + "allOf": [ + {"$ref": "#/$defs/Base"}, + { + "type": "object", + "properties": {"parent": {"$ref": "#/$defs/Extended"}}, + }, + ] }, - "$ref": "#/$defs/NonExistentType" - } - # Should handle gracefully without crashing - result = _dereference_schema(schema) - assert result is not None - - def test_deeply_nested_non_circular_refs(self): - """Test deeply nested but non-circular reference chain.""" - schema = { - "$defs": { - "Level1": { - "type": "object", - "properties": { - "level2": {"$ref": "#/$defs/Level2"} - } + }, + "$ref": "#/$defs/Extended", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_with_oneof(self): + """Test circular reference inside oneOf composition.""" + schema = { + "$defs": { + "PolymorphicNode": { + "type": "object", + "properties": { + "data": {"type": "string"}, + "next": { + "oneOf": [ + {"$ref": "#/$defs/PolymorphicNode"}, + {"type": "string"}, + {"type": "null"}, + ] + }, }, - "Level2": { - "type": "object", - "properties": { - "level3": {"$ref": "#/$defs/Level3"} - } + } + }, + "$ref": "#/$defs/PolymorphicNode", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_multiple_independent_circular_refs(self): + """Test schema with multiple independent circular structures.""" + schema = { + "$defs": { + "Tree": { + "type": "object", + "properties": { + "value": {"type": "integer"}, + "children": { + "type": "array", + "items": {"$ref": "#/$defs/Tree"}, + }, }, - "Level3": { - "type": "object", - "properties": { - "value": {"type": "string"} - } - } }, - "$ref": "#/$defs/Level1" - } - - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - - def test_circular_ref_with_additional_properties(self): - """Test circular reference in additionalProperties.""" - schema = { - "$defs": { - "DynamicNode": { - "type": "object", - "properties": { - "name": {"type": "string"} + "Graph": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "neighbors": { + "type": "array", + "items": {"$ref": "#/$defs/Graph"}, }, - "additionalProperties": {"$ref": "#/$defs/DynamicNode"} - } + }, }, - "$ref": "#/$defs/DynamicNode" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_self_referencing_with_nullable(self): - """Test self-reference with nullable field (optional next pointer).""" - schema = { - "$defs": { - "LinkedList": { - "type": "object", - "properties": { - "data": {"type": "string"}, - "next": { - "anyOf": [ - {"$ref": "#/$defs/LinkedList"}, - {"type": "null"} - ] - } - } - } + }, + "type": "object", + "properties": { + "tree": {"$ref": "#/$defs/Tree"}, + "graph": {"$ref": "#/$defs/Graph"}, + }, + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_empty_schema(self): + """Test empty schema doesn't cause issues.""" + schema = {} + result = _dereference_schema(schema) + assert result == {} + + def test_schema_without_refs(self): + """Test schema without any $ref works normally.""" + schema = { + "type": "object", + "properties": {"name": {"type": "string"}, "age": {"type": "integer"}}, + } + result = _dereference_schema(schema) + assert result == schema + + def test_invalid_ref_graceful_handling(self): + """Test graceful handling of invalid $ref.""" + schema = { + "$defs": { + "ValidType": { + "type": "object", + "properties": {"field": {"type": "string"}}, + } + }, + "$ref": "#/$defs/NonExistentType", + } + # Should handle gracefully without crashing + result = _dereference_schema(schema) + assert result is not None + + def test_deeply_nested_non_circular_refs(self): + """Test deeply nested but non-circular reference chain.""" + schema = { + "$defs": { + "Level1": { + "type": "object", + "properties": {"level2": {"$ref": "#/$defs/Level2"}}, }, - "$ref": "#/$defs/LinkedList" - } - - result = _dereference_schema(schema) - assert result is not None - - def test_circular_ref_reproducing_issue_3870(self): - """Reproduce the exact scenario from issue #3870.""" - # This is the type of schema that would cause RecursionError before the fix - schema = { - "$defs": { - "RecursiveType": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "children": { - "type": "array", - "items": {"$ref": "#/$defs/RecursiveType"} - } - } - } + "Level2": { + "type": "object", + "properties": {"level3": {"$ref": "#/$defs/Level3"}}, }, - "type": "object", - "properties": { - "root": {"$ref": "#/$defs/RecursiveType"} + "Level3": { + "type": "object", + "properties": {"value": {"type": "string"}}, + }, + }, + "$ref": "#/$defs/Level1", + } + + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + + def test_circular_ref_with_additional_properties(self): + """Test circular reference in additionalProperties.""" + schema = { + "$defs": { + "DynamicNode": { + "type": "object", + "properties": {"name": {"type": "string"}}, + "additionalProperties": {"$ref": "#/$defs/DynamicNode"}, + } + }, + "$ref": "#/$defs/DynamicNode", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_self_referencing_with_nullable(self): + """Test self-reference with nullable field (optional next pointer).""" + schema = { + "$defs": { + "LinkedList": { + "type": "object", + "properties": { + "data": {"type": "string"}, + "next": { + "anyOf": [ + {"$ref": "#/$defs/LinkedList"}, + {"type": "null"}, + ] + }, + }, + } + }, + "$ref": "#/$defs/LinkedList", + } + + result = _dereference_schema(schema) + assert result is not None + + def test_circular_ref_reproducing_issue_3870(self): + """Reproduce the exact scenario from issue #3870.""" + # This is the type of schema that would cause RecursionError before the fix + schema = { + "$defs": { + "RecursiveType": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "children": { + "type": "array", + "items": {"$ref": "#/$defs/RecursiveType"}, + }, + }, } - } - - # Before the fix, this would raise RecursionError - # After the fix, it should complete successfully - result = _dereference_schema(schema) - assert result is not None - assert "properties" in result - assert "root" in result["properties"] + }, + "type": "object", + "properties": {"root": {"$ref": "#/$defs/RecursiveType"}}, + } + + # Before the fix, this would raise RecursionError + # After the fix, it should complete successfully + result = _dereference_schema(schema) + assert result is not None + assert "properties" in result + assert "root" in result["properties"]