Skip to content

Commit 737281a

Browse files
authored
Add bundled schema with $id and update local fragment resolution logic (#2646)
1 parent 9b7863d commit 737281a

5 files changed

Lines changed: 164 additions & 1 deletion

File tree

src/datamodel_code_generator/reference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ def resolve_ref(self, path: Sequence[str] | str) -> str: # noqa: PLR0911, PLR09
585585
else:
586586
if "#" not in joined_path:
587587
joined_path += "#"
588-
elif joined_path[0] == "#" and not self.base_url:
588+
elif joined_path[0] == "#" and self.current_root:
589589
joined_path = f"{'/'.join(self.current_root)}{joined_path}"
590590

591591
file_path, fragment = joined_path.split("#", 1)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# generated by datamodel-codegen:
2+
# filename: bundled_schema_with_id.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import Enum
8+
from typing import Optional
9+
10+
from pydantic import BaseModel
11+
12+
13+
class Species(Enum):
14+
dog = 'dog'
15+
cat = 'cat'
16+
bird = 'bird'
17+
18+
19+
class Pet(BaseModel):
20+
name: str
21+
species: Species
22+
23+
24+
class User(BaseModel):
25+
name: str
26+
pet: Optional[Pet] = None
27+
28+
29+
class BundledSchema(BaseModel):
30+
user: Optional[User] = None
31+
pet: Optional[Pet] = None
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://example.org/schemas/bundled.json",
4+
"title": "Bundled Schema",
5+
"description": "A bundled schema with $id that differs from fetch URL (Issue #1798)",
6+
"type": "object",
7+
"properties": {
8+
"user": {
9+
"$ref": "#/definitions/user"
10+
},
11+
"pet": {
12+
"$ref": "#/definitions/pet"
13+
}
14+
},
15+
"definitions": {
16+
"user": {
17+
"type": "object",
18+
"properties": {
19+
"name": {
20+
"type": "string"
21+
},
22+
"pet": {
23+
"$ref": "#/definitions/pet"
24+
}
25+
},
26+
"required": ["name"]
27+
},
28+
"pet": {
29+
"type": "object",
30+
"properties": {
31+
"name": {
32+
"type": "string"
33+
},
34+
"species": {
35+
"$ref": "#/definitions/species"
36+
}
37+
},
38+
"required": ["name", "species"]
39+
},
40+
"species": {
41+
"type": "string",
42+
"enum": ["dog", "cat", "bird"]
43+
}
44+
}
45+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3610,3 +3610,59 @@ def test_main_jsonschema_reserved_field_name_pydantic(output_file: Path) -> None
36103610
"3.11",
36113611
],
36123612
)
3613+
3614+
3615+
@pytest.mark.benchmark
3616+
@LEGACY_BLACK_SKIP
3617+
def test_main_bundled_schema_with_id_local_file(output_file: Path) -> None:
3618+
"""Test bundled schema with $id using local file input (Issue #1798)."""
3619+
run_main_and_assert(
3620+
input_path=JSON_SCHEMA_DATA_PATH / "bundled_schema_with_id.json",
3621+
output_path=output_file,
3622+
input_file_type="jsonschema",
3623+
assert_func=assert_file_content,
3624+
expected_file="bundled_schema_with_id.py",
3625+
extra_args=[
3626+
"--output-model-type",
3627+
"pydantic_v2.BaseModel",
3628+
],
3629+
)
3630+
3631+
3632+
@pytest.mark.benchmark
3633+
@LEGACY_BLACK_SKIP
3634+
def test_main_bundled_schema_with_id_url(mocker: MockerFixture, output_file: Path) -> None:
3635+
"""Test bundled schema with $id using URL input produces same output as local file."""
3636+
schema_path = JSON_SCHEMA_DATA_PATH / "bundled_schema_with_id.json"
3637+
3638+
mock_response = mocker.Mock()
3639+
mock_response.text = schema_path.read_text()
3640+
3641+
httpx_get_mock = mocker.patch(
3642+
"httpx.get",
3643+
return_value=mock_response,
3644+
)
3645+
3646+
run_main_url_and_assert(
3647+
url="https://cdn.example.com/schemas/bundled_schema_with_id.json",
3648+
output_path=output_file,
3649+
input_file_type="jsonschema",
3650+
assert_func=assert_file_content,
3651+
expected_file="bundled_schema_with_id.py",
3652+
extra_args=[
3653+
"--output-model-type",
3654+
"pydantic_v2.BaseModel",
3655+
],
3656+
transform=lambda s: s.replace(
3657+
"# filename: https://cdn.example.com/schemas/bundled_schema_with_id.json",
3658+
"# filename: bundled_schema_with_id.json",
3659+
),
3660+
)
3661+
3662+
httpx_get_mock.assert_called_once_with(
3663+
"https://cdn.example.com/schemas/bundled_schema_with_id.json",
3664+
headers=None,
3665+
verify=True,
3666+
follow_redirects=True,
3667+
params=None,
3668+
)

tests/test_reference.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,34 @@ def test_url_ref_matches_local_id_preserves_empty_json_pointer_token() -> None:
325325
result = resolver.resolve_ref("https://example.org/types#/items//child")
326326

327327
assert result == "#/$defs/types/items//child"
328+
329+
330+
def test_resolve_ref_local_fragment_with_base_url_and_current_root() -> None:
331+
"""Local fragment refs should resolve to current_root when it's set, even with base_url (Issue #1798)."""
332+
resolver = ModelResolver(base_url="https://raw.githubusercontent.com/user/repo/schema.json")
333+
resolver.set_root_id("https://cveproject.github.io/schema/schema.json")
334+
resolver.set_current_root(["https://raw.githubusercontent.com/user/repo/schema.json"])
335+
336+
result = resolver.resolve_ref("#/definitions/Foo")
337+
338+
assert result == "https://raw.githubusercontent.com/user/repo/schema.json#/definitions/Foo"
339+
340+
341+
def test_resolve_ref_local_fragment_with_different_host_base_url_and_root_id() -> None:
342+
"""Local fragment refs should resolve correctly when base_url and root_id have different hosts (Issue #1798)."""
343+
resolver = ModelResolver(base_url="https://raw.githubusercontent.com/user/repo/schema.json")
344+
resolver.set_root_id("https://cveproject.github.io/schema/schema.json")
345+
resolver.set_current_root(["https://raw.githubusercontent.com/user/repo/schema.json"])
346+
347+
result = resolver.resolve_ref("#/definitions/product/properties/url")
348+
349+
assert result == "https://raw.githubusercontent.com/user/repo/schema.json#/definitions/product/properties/url"
350+
351+
352+
def test_resolve_ref_local_fragment_without_current_root_falls_back_to_url() -> None:
353+
"""Local fragment refs without current_root should fall back to URL resolution (Issue #1798)."""
354+
resolver = ModelResolver(base_url="https://example.com/schemas/main.json")
355+
356+
result = resolver.resolve_ref("#/definitions/Foo")
357+
358+
assert result == "https://example.com/schemas/main.json#/definitions/Foo"

0 commit comments

Comments
 (0)