fix: handle circular JSON Pointer $ref in dereference_refs (#3896)

Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
This commit is contained in:
chaoliang yan 2026-04-21 03:00:18 +10:00 committed by GitHub
commit 5009d64465
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 41 additions and 2 deletions

View file

@ -172,8 +172,11 @@ def dereference_refs(schema: dict[str, Any]) -> dict[str, Any]:
return dereferenced
except JsonRefError:
# Self-referencing/circular schemas can't be fully dereferenced
except (JsonRefError, RecursionError):
# Self-referencing/circular schemas can't be fully dereferenced.
# RecursionError covers circular $ref using JSON Pointer paths
# (e.g. "#/properties/nodes/items") that bypass $defs-based cycle
# detection — common in schemas from .NET/System.Text.Json.
# Fall back to resolving only root-level $ref (for MCP spec compliance)
return resolve_root_ref(schema)

View file

@ -128,6 +128,42 @@ class TestDereferenceRefs:
assert result.get("type") == "object"
assert "$defs" in result # $defs preserved for circular refs
def test_falls_back_for_circular_json_pointer_refs(self):
"""Test that circular JSON Pointer $ref (non-$defs) does not crash.
.NET/System.Text.Json emits $ref with JSON Pointer paths like
#/properties/nodes/items instead of $defs-based references.
Circular pointers must not cause a RecursionError.
"""
schema = {
"type": "object",
"properties": {
"nodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/properties/nodes/items"},
},
},
},
},
},
}
result = dereference_refs(schema)
# Should return the schema without crashing; circular refs stay unresolved
assert result["type"] == "object"
assert "properties" in result
# The circular $ref should be preserved (not inlined)
children_items = result["properties"]["nodes"]["items"]["properties"][
"children"
]["items"]
assert children_items["$ref"] == "#/properties/nodes/items"
def test_preserves_sibling_keywords(self):
"""Test that sibling keywords (default, description) are preserved.