Skip to content

Commit d7a1f71

Browse files
authored
Fix relative URL refs with path-only root ids (#3085)
1 parent f0960e9 commit d7a1f71

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

src/datamodel_code_generator/reference.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ def add_id(self, id_: str, path: Sequence[str]) -> None:
627627
"""Register an identifier mapping to a resolved reference path."""
628628
self.ids["/".join(self.current_root)][id_] = self.resolve_ref(path)
629629

630-
def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR0912, PLR0914
630+
def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR0912, PLR0914, PLR0915
631631
"""Resolve a reference path to its canonical form."""
632632
joined_path = path if isinstance(path, str) else self.join_path(tuple(path))
633633
if joined_path == "#":
@@ -679,7 +679,9 @@ def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR09
679679
if self.base_url:
680680
from .http import join_url # noqa: PLC0415
681681

682-
effective_base = self.root_id or self.base_url
682+
effective_base = self.base_url
683+
if self.root_id:
684+
effective_base = self.root_id if is_url(self.root_id) else join_url(self.base_url, self.root_id)
683685
joined_url = join_url(effective_base, ref)
684686
if "#" in joined_url:
685687
return joined_url

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,73 @@ def test_main_root_id_jsonschema_with_absolute_local_file(output_file: Path) ->
992992
)
993993

994994

995+
def test_main_url_with_relative_root_id_resolves_relative_refs(mocker: MockerFixture, tmp_path: Path) -> None:
996+
"""Test --url input keeps resolving relative refs remotely when root $id is path-only."""
997+
main_response = mocker.Mock()
998+
main_response.status_code = 200
999+
main_response.headers = {}
1000+
main_response.text = json.dumps({
1001+
"$id": "/schemas/v1/main.schema.json",
1002+
"$schema": "http://json-schema.org/draft-07/schema#",
1003+
"title": "Main",
1004+
"type": "object",
1005+
"properties": {
1006+
"sub": {
1007+
"$ref": "sub.schema.json",
1008+
}
1009+
},
1010+
"required": ["sub"],
1011+
})
1012+
sub_response = mocker.Mock()
1013+
sub_response.status_code = 200
1014+
sub_response.headers = {}
1015+
sub_response.text = json.dumps({
1016+
"$id": "/schemas/v1/sub.schema.json",
1017+
"$schema": "http://json-schema.org/draft-07/schema#",
1018+
"title": "Sub",
1019+
"type": "string",
1020+
"pattern": "^[0-9a-f]{8}$",
1021+
})
1022+
httpx_get_mock = mocker.patch("httpx.get", side_effect=[main_response, sub_response])
1023+
output_dir = tmp_path / "output"
1024+
1025+
result = run_main_with_args([
1026+
"--url",
1027+
"http://localhost:8888/schemas/v1/main.schema.json",
1028+
"--output",
1029+
str(output_dir),
1030+
"--input-file-type",
1031+
"jsonschema",
1032+
"--output-model-type",
1033+
"pydantic_v2.BaseModel",
1034+
])
1035+
1036+
assert result == Exit.OK
1037+
main_content = (output_dir / "__init__.py").read_text(encoding="utf-8")
1038+
sub_content = (output_dir / "sub.py").read_text(encoding="utf-8")
1039+
assert "class Main(BaseModel):" in main_content
1040+
assert "sub: sub_1.Schema" in main_content
1041+
assert "class Schema(RootModel[constr(pattern=r'^[0-9a-f]{8}$')]):" in sub_content
1042+
httpx_get_mock.assert_has_calls([
1043+
call(
1044+
"http://localhost:8888/schemas/v1/main.schema.json",
1045+
headers=None,
1046+
verify=True,
1047+
follow_redirects=True,
1048+
params=None,
1049+
timeout=30.0,
1050+
),
1051+
call(
1052+
"http://localhost:8888/schemas/v1/sub.schema.json",
1053+
headers=None,
1054+
verify=True,
1055+
follow_redirects=True,
1056+
params=None,
1057+
timeout=30.0,
1058+
),
1059+
])
1060+
1061+
9951062
def test_main_remote_ref_emits_deprecation_warning(mocker: MockerFixture, tmp_path: Path) -> None:
9961063
"""Test that implicit remote $ref fetching emits a FutureWarning when flag is not set."""
9971064
person_response = mocker.Mock()

tests/test_reference.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,16 @@ def test_resolve_ref_with_root_id_differs_from_base_url() -> None:
213213
assert result == "https://example.com/common/types.json#"
214214

215215

216+
def test_resolve_ref_with_relative_root_id_and_base_url() -> None:
217+
"""Relative root $id should be resolved against the retrieval URL before $ref resolution."""
218+
resolver = ModelResolver(base_url="http://localhost:8888/schemas/v1/main.schema.json")
219+
resolver.set_root_id("/schemas/v1/main.schema.json")
220+
221+
result = resolver.resolve_ref("sub.schema.json")
222+
223+
assert result == "http://localhost:8888/schemas/v1/sub.schema.json#"
224+
225+
216226
@pytest.mark.parametrize(
217227
("base_url", "ref", "expected"),
218228
[

0 commit comments

Comments
 (0)