Skip to content

Commit 9ed0742

Browse files
committed
Address local HTTP ref review
1 parent 3683fb2 commit 9ed0742

8 files changed

Lines changed: 80 additions & 33 deletions

File tree

docs/cli-reference/general-options.md

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1638,7 +1638,9 @@ Resolve HTTP references from local schema files.
16381638

16391639
The `--http-local-ref-path` flag maps HTTP(S) `$ref` URLs to files under
16401640
a local schema store instead of fetching them from the network. The host and
1641-
URL path are used as the relative path under the schema store.
1641+
URL path are used as the relative path under the schema store. For example,
1642+
`https://api.example.com/schemas/pet.json` is read from
1643+
`schemas/api.example.com/schemas/pet.json`.
16421644

16431645
!!! tip "Usage"
16441646

@@ -1655,19 +1657,7 @@ URL path are used as the relative path under the schema store.
16551657
```json
16561658
{
16571659
"$schema": "http://json-schema.org/draft-07/schema#",
1658-
"title": "Pet",
1659-
"type": "object",
1660-
"properties": {
1661-
"id": {
1662-
"type": "integer"
1663-
},
1664-
"name": {
1665-
"type": "string"
1666-
},
1667-
"tag": {
1668-
"type": "string"
1669-
}
1670-
}
1660+
"$ref": "https://api.example.com/schemas/pet.json"
16711661
}
16721662
```
16731663

@@ -1680,13 +1670,17 @@ URL path are used as the relative path under the schema store.
16801670

16811671
from __future__ import annotations
16821672

1683-
from pydantic import BaseModel
1673+
from pydantic import BaseModel, RootModel
16841674

16851675

16861676
class Pet(BaseModel):
16871677
id: int | None = None
16881678
name: str | None = None
16891679
tag: str | None = None
1680+
1681+
1682+
class Model(RootModel[Pet]):
1683+
root: Pet
16901684
```
16911685

16921686
---

docs/cli-reference/model-customization.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5272,9 +5272,9 @@ Use default values from schema in generated models.
52725272
The `--use-default` flag allows required fields with default values to be generated
52735273
with their defaults, making them optional to provide when instantiating the model.
52745274

5275-
The field type still follows the schema's nullability. For example, a required
5276-
string field with a default is generated as `str = 'value'`, not
5277-
`str | None = 'value'`, unless the schema allows null.
5275+
When `--strict-nullable` is enabled, the field type still follows the schema's
5276+
nullability. For example, a required string field with a default is generated
5277+
as `str = 'value'`, not `str | None = 'value'`, unless the schema allows null.
52785278

52795279
**Related:** [`--strict-nullable`](model-customization.md#strict-nullable)
52805280

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3850,14 +3850,19 @@ def _get_ref_body_from_local_http_path(self, ref: str) -> dict[str, YamlValue]:
38503850
raise Error(msg)
38513851

38523852
parts = [unquote(part) for part in parsed.path.split("/") if part]
3853-
if not parsed.netloc or any(part in {".", ".."} for part in parts):
3853+
if not parsed.netloc or any(part in {".", ".."} or "/" in part or "\\" in part for part in parts):
38543854
msg = f"Unsupported local HTTP $ref URL path: {ref}"
38553855
raise Error(msg)
38563856

3857+
base_path = self.http_local_ref_path.resolve()
38573858
relative_path = Path(parsed.netloc, *parts)
3858-
file_paths = [self.http_local_ref_path / relative_path]
3859+
file_paths = [(base_path / relative_path).resolve()]
38593860
if not parts or not Path(parts[-1]).suffix:
3860-
file_paths.append(file_paths[0].with_name(f"{file_paths[0].name}.json"))
3861+
file_paths.append((base_path / relative_path.with_name(f"{relative_path.name}.json")).resolve())
3862+
3863+
if any(not file_path.is_relative_to(base_path) for file_path in file_paths):
3864+
msg = f"Unsupported local HTTP $ref URL path: {ref}"
3865+
raise Error(msg)
38613866

38623867
for file_path in file_paths:
38633868
if file_path.is_file():
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# generated by datamodel-codegen:
2+
# filename: https://api.example.com/schema.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import BaseModel, RootModel
8+
9+
10+
class Pet(BaseModel):
11+
id: int | None = None
12+
name: str | None = None
13+
tag: str | None = None
14+
15+
16+
class Model(RootModel[Pet]):
17+
root: Pet
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$ref": "https://api.example.com/schemas/pet.json"
4+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -510,9 +510,9 @@ def test_main_null_and_array(output_file: Path) -> None:
510510
The `--use-default` flag allows required fields with default values to be generated
511511
with their defaults, making them optional to provide when instantiating the model.
512512
513-
The field type still follows the schema's nullability. For example, a required
514-
string field with a default is generated as `str = 'value'`, not
515-
`str | None = 'value'`, unless the schema allows null.""",
513+
When `--strict-nullable` is enabled, the field type still follows the schema's
514+
nullability. For example, a required string field with a default is generated
515+
as `str = 'value'`, not `str | None = 'value'`, unless the schema allows null.""",
516516
input_schema="jsonschema/use_default_with_const.json",
517517
cli_args=["--output-model-type", "pydantic_v2.BaseModel", "--use-default"],
518518
golden_output="jsonschema/use_default_with_const.py",
@@ -524,9 +524,9 @@ def test_use_default_pydantic_v2_with_json_schema_const(output_file: Path) -> No
524524
The `--use-default` flag allows required fields with default values to be generated
525525
with their defaults, making them optional to provide when instantiating the model.
526526
527-
The field type still follows the schema's nullability. For example, a required
528-
string field with a default is generated as `str = 'value'`, not
529-
`str | None = 'value'`, unless the schema allows null.
527+
When `--strict-nullable` is enabled, the field type still follows the schema's
528+
nullability. For example, a required string field with a default is generated
529+
as `str = 'value'`, not `str | None = 'value'`, unless the schema allows null.
530530
"""
531531
run_main_and_assert(
532532
input_path=JSON_SCHEMA_DATA_PATH / "use_default_with_const.json",

tests/parser/test_jsonschema.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ def test_json_schema_ref_url_from_local_http_path_with_extension(tmp_path: Path,
289289
[
290290
"http:///application/package/element/sub-element",
291291
"http://example.com/application/package/../sub-element",
292+
"http://example.com/..%5C..%5CWindows%5Cwin.ini",
293+
"http://example.com/path%2Fwith-slash",
292294
],
293295
)
294296
def test_json_schema_ref_url_from_local_http_path_invalid_path(tmp_path: Path, ref: str) -> None:
@@ -307,6 +309,24 @@ def test_json_schema_ref_url_from_local_http_path_missing_file(tmp_path: Path) -
307309
parser._get_ref_body_from_url("http://example.com/schema")
308310

309311

312+
def test_json_schema_ref_url_from_local_http_path_symlink_escape(tmp_path: Path) -> None:
313+
"""Test local HTTP JSON schema references cannot escape the schema store through symlinks."""
314+
schema_store = tmp_path / "schemas"
315+
local_schema = schema_store / "example.com" / "schema.json"
316+
local_schema.parent.mkdir(parents=True)
317+
outside_schema = tmp_path / "outside.json"
318+
outside_schema.write_text('{"type": "object"}', encoding="utf-8")
319+
try:
320+
local_schema.symlink_to(outside_schema)
321+
except OSError as exc:
322+
pytest.skip(f"symlink creation is not supported: {exc}")
323+
324+
parser = JsonSchemaParser("", allow_remote_refs=False, http_local_ref_path=schema_store)
325+
326+
with pytest.raises(Error, match="Unsupported local HTTP \\$ref URL path"):
327+
parser._get_ref_body_from_url("http://example.com/schema.json")
328+
329+
310330
@pytest.mark.parametrize(
311331
("source_obj", "generated_classes"),
312332
[

tests/test_main_kr.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1543,15 +1543,17 @@ def test_url_with_http_headers(mock_httpx_get: HttpxGetMockFactory, output_file:
15431543
15441544
The `--http-local-ref-path` flag maps HTTP(S) `$ref` URLs to files under
15451545
a local schema store instead of fetching them from the network. The host and
1546-
URL path are used as the relative path under the schema store.""",
1547-
input_schema="jsonschema/pet_simple.json",
1546+
URL path are used as the relative path under the schema store. For example,
1547+
`https://api.example.com/schemas/pet.json` is read from
1548+
`schemas/api.example.com/schemas/pet.json`.""",
1549+
input_schema="jsonschema/http_local_ref_path_root.json",
15481550
cli_args=[
15491551
"--url",
15501552
"https://api.example.com/schema.json",
15511553
"--http-local-ref-path",
15521554
"schemas",
15531555
],
1554-
golden_output="main_kr/url_with_headers/output.py",
1556+
golden_output="main_kr/http_local_ref_path/output.py",
15551557
)
15561558
@freeze_time("2019-07-26")
15571559
def test_http_local_ref_path_cli_doc(mock_httpx_get: HttpxGetMockFactory, output_file: Path, tmp_path: Path) -> None:
@@ -1562,17 +1564,22 @@ def test_http_local_ref_path_cli_doc(mock_httpx_get: HttpxGetMockFactory, output
15621564
URL path are used as the relative path under the schema store.
15631565
"""
15641566
schema_store = tmp_path / "schemas"
1565-
schema_store.mkdir()
1567+
local_schema = schema_store / "api.example.com" / "schemas" / "pet.json"
1568+
local_schema.parent.mkdir(parents=True)
1569+
local_schema.write_text((JSON_SCHEMA_DATA_PATH / "pet_simple.json").read_text(), encoding="utf-8")
15661570
mock_get = mock_httpx_get(
1567-
MockHttpxResponse("https://api.example.com/schema.json", JSON_SCHEMA_DATA_PATH / "pet_simple.json")
1571+
MockHttpxResponse(
1572+
"https://api.example.com/schema.json",
1573+
JSON_SCHEMA_DATA_PATH / "http_local_ref_path_root.json",
1574+
)
15681575
)
15691576

15701577
run_main_url_and_assert(
15711578
url="https://api.example.com/schema.json",
15721579
output_path=output_file,
15731580
input_file_type="jsonschema",
15741581
assert_func=assert_file_content,
1575-
expected_file=EXPECTED_MAIN_KR_PATH / "url_with_headers" / "output.py",
1582+
expected_file=EXPECTED_MAIN_KR_PATH / "http_local_ref_path" / "output.py",
15761583
extra_args=["--http-local-ref-path", str(schema_store)],
15771584
)
15781585
assert_httpx_get_kwargs(mock_get)

0 commit comments

Comments
 (0)