Skip to content

Commit e717208

Browse files
authored
Fix allOf array property merging to preserve child $ref (#2962)
* Fix allOf array property merging to preserve child $ref * Fix filename mismatch in test fixture * Rename test fixtures to descriptive names
1 parent ae89a00 commit e717208

4 files changed

Lines changed: 128 additions & 13 deletions

File tree

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1870,7 +1870,10 @@ def _merge_property_schemas(self, parent_dict: dict[str, Any], child_dict: dict[
18701870

18711871
for key, value in child_dict.items():
18721872
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
1873-
result[key] = self._merge_property_schemas(result[key], value)
1873+
if "$ref" in value:
1874+
result[key] = value
1875+
else:
1876+
result[key] = self._merge_property_schemas(result[key], value)
18741877
else:
18751878
result[key] = value
18761879
return result
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# generated by datamodel-codegen:
2+
# filename: allof_array_ref_override.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Any
8+
from uuid import UUID
9+
10+
from pydantic import AwareDatetime, BaseModel, Field, conint
11+
12+
13+
class DataType(BaseModel):
14+
property_1: UUID | None = Field(
15+
None, description='Unique identifier for the installation.'
16+
)
17+
property_2: str | None = Field(None, description='Description of the installation.')
18+
property_3: AwareDatetime | None = Field(
19+
None, description='Timestamp when the installation was created.'
20+
)
21+
22+
23+
class Metadata(BaseModel):
24+
limit: conint(ge=1, le=100) = Field(
25+
..., description='Number of data types returned in this response', examples=[20]
26+
)
27+
page: conint(ge=1) = Field(
28+
..., description='The page number to retrieve', examples=[2]
29+
)
30+
31+
32+
class CollectionWrapper(BaseModel):
33+
"""
34+
Generic response wrapper containing a collection of items and pagination metadata.
35+
"""
36+
37+
data: list[dict[str, Any]] = Field(
38+
..., description='Array of items in the collection.'
39+
)
40+
pagination: Metadata = Field(
41+
..., description='Pagination metadata for the collection.'
42+
)
43+
44+
45+
class PaginatedDataTypeList(CollectionWrapper):
46+
data: list[DataType] | None = Field(
47+
None, description='Array of items in the collection.'
48+
)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
openapi: "3.0.0"
2+
info:
3+
title: Issue 2953 Test
4+
version: "1.0.0"
5+
components:
6+
schemas:
7+
Collection-Wrapper:
8+
description: Generic response wrapper containing a collection of items and pagination metadata.
9+
type: object
10+
required:
11+
- data
12+
- pagination
13+
properties:
14+
data:
15+
description: Array of items in the collection.
16+
type: array
17+
items:
18+
type: object
19+
pagination:
20+
description: Pagination metadata for the collection.
21+
$ref: "#/components/schemas/Metadata"
22+
DataType:
23+
type: object
24+
required:
25+
- id
26+
properties:
27+
property_1:
28+
type: string
29+
format: uuid
30+
readOnly: true
31+
description: Unique identifier for the installation.
32+
property_2:
33+
type: string
34+
description: Description of the installation.
35+
property_3:
36+
type: string
37+
format: date-time
38+
readOnly: true
39+
description: Timestamp when the installation was created.
40+
41+
PaginatedDataTypeList:
42+
allOf:
43+
- properties:
44+
data:
45+
type: array
46+
items:
47+
$ref: "#/components/schemas/DataType"
48+
- $ref: "#/components/schemas/Collection-Wrapper"
49+
Metadata:
50+
type: object
51+
required:
52+
- limit
53+
- page
54+
properties:
55+
limit:
56+
type: integer
57+
minimum: 1
58+
maximum: 100
59+
description: Number of data types returned in this response
60+
example: 20
61+
page:
62+
type: integer
63+
minimum: 1
64+
description: The page number to retrieve
65+
example: 2

tests/main/openapi/test_main_openapi.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4942,25 +4942,24 @@ def test_main_openapi_deprecated_field(output_file: Path) -> None:
49424942

49434943

49444944
@SKIP_PYDANTIC_V1
4945-
def test_main_openapi_reuse_scope_tree_single_file_error(capsys: pytest.CaptureFixture[str], output_file: Path) -> None:
4946-
"""Test --reuse-scope=tree with single file output raises proper error (#2953).
4945+
def test_main_openapi_allof_array_ref_no_duplicate_model(output_file: Path) -> None:
4946+
"""Test allOf with array property referencing another schema (#2959).
49474947
4948-
When using reuse-scope=tree with single file output and duplicate models,
4949-
it should raise a proper error about needing a directory output instead of
4950-
crashing with IndexError.
4948+
When allOf merges an array property from parent (with generic items) and child
4949+
(with $ref items), the child's $ref should completely override the parent,
4950+
preventing duplicate model generation like 'Datum' class.
49514951
"""
49524952
run_main_and_assert(
4953-
input_path=OPEN_API_DATA_PATH / "issue_2953.yaml",
4953+
input_path=OPEN_API_DATA_PATH / "allof_array_ref_override.yaml",
49544954
output_path=output_file,
49554955
input_file_type="openapi",
4956-
expected_exit=Exit.ERROR,
4957-
capsys=capsys,
4958-
expected_stderr_contains="Modular references require an output directory, not a file",
4956+
assert_func=assert_file_content,
4957+
expected_file="allof_array_ref_override.py",
49594958
extra_args=[
49604959
"--output-model-type",
49614960
"pydantic_v2.BaseModel",
4962-
"--reuse-model",
4963-
"--reuse-scope",
4964-
"tree",
4961+
"--use-standard-collections",
4962+
"--use-union-operator",
4963+
"--use-schema-description",
49654964
],
49664965
)

0 commit comments

Comments
 (0)