-
Notifications
You must be signed in to change notification settings - Fork 2.9k
fix: Detect and handle circular $ref in _dereference_schema
#3880
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 43 commits
a17a8d2
17965d1
81c75c9
6721fbd
2e40cb0
7c1e6ce
b029f36
58aa121
d104412
8f1782f
6fda596
813cfd6
9c8e2f2
b689025
edde80d
cedc98e
17276dc
da827fb
3b20062
c2f2158
6f654d1
4ba5ba3
1fee3b4
1419043
ce42716
84a6337
041909c
bb890fa
273e723
4e59113
7e63e87
84374df
d287f82
fab5b2c
7235f4e
1fd974b
109a279
35dbd12
628b300
6b6a860
caa62c4
487c557
59653ae
465ddc7
5e55d87
10f16b1
a2555fc
7d989d3
91455d3
242f5da
043ee0c
95fdf8d
cb1ffce
8292177
6c92737
51c821a
d66af95
4ed165b
fa8c4fd
33dc71f
3aa275a
1be0082
57a575b
d230fad
c61bf0f
8f079ed
3db54a8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,11 +107,47 @@ 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_refs(sub_schema: Any) -> Any: | ||
| 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): | ||
| return None | ||
| current = current.get(part) | ||
| if current is None: | ||
| return None | ||
|
Comment on lines
123
to
137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
This can lead to incorrect reference resolution for valid schemas. I suggest expanding the implementation to cover these cases for a more robust solution. for part in parts:
# Unescape JSON Pointer path parts (~1 -> /, ~0 -> ~)
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:
# Cannot traverse further
return None
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added to make the JSON pointer handling more robust |
||
|
|
||
| return current | ||
|
|
||
| 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() | ||
|
|
@@ -120,16 +156,38 @@ 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.