Skip to content

Commit 78a2903

Browse files
authored
Fix allOf array items partial override to inherit parent item types instead of Any (#2667)
* Fix allOf array item type inheritance for partial overrides * Add test for OpenAPI allOf partial override with List[Any] and update related files * Fix type inheritance for nested array items in allOf partial overrides
1 parent efd679a commit 78a2903

14 files changed

Lines changed: 371 additions & 1 deletion

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,27 @@ def _build_lightweight_type( # noqa: PLR0911, PLR0912
13441344

13451345
return None
13461346

1347+
def _is_list_with_any_item_type(self, data_type: DataType | None) -> bool: # noqa: PLR6301
1348+
"""Return True when data_type represents List[Any] (including nested lists)."""
1349+
if not data_type: # pragma: no cover
1350+
return False
1351+
1352+
candidate = data_type
1353+
if not candidate.is_list and len(candidate.data_types) == 1 and candidate.data_types[0].is_list:
1354+
candidate = candidate.data_types[0]
1355+
1356+
if not candidate.is_list or len(candidate.data_types) != 1:
1357+
return False
1358+
1359+
item_type = candidate.data_types[0]
1360+
while len(item_type.data_types) == 1:
1361+
inner = item_type.data_types[0]
1362+
if (not item_type.is_list and inner.is_list) or item_type.is_list:
1363+
item_type = inner
1364+
else:
1365+
break
1366+
return item_type.type == ANY
1367+
13471368
def _get_inherited_field_type(self, prop_name: str, base_classes: list[Reference]) -> DataType | None:
13481369
"""Get the data type for an inherited property from parent schemas."""
13491370
for base in base_classes:
@@ -1497,7 +1518,7 @@ def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs
14971518
kwargs.pop("dataclass_arguments", None)
14981519
return data_model_class(**kwargs)
14991520

1500-
def _parse_object_common_part( # noqa: PLR0912, PLR0913
1521+
def _parse_object_common_part( # noqa: PLR0912, PLR0913, PLR0915
15011522
self,
15021523
name: str,
15031524
obj: JsonSchemaObject,
@@ -1539,6 +1560,37 @@ def _parse_object_common_part( # noqa: PLR0912, PLR0913
15391560
if new_type.kwargs is None and current_type.kwargs is not None: # pragma: no cover
15401561
new_type.kwargs = current_type.kwargs
15411562
field.data_type = new_type
1563+
# Handle List[Any] case: inherit item type from parent if items have Any type
1564+
elif field_name and self._is_list_with_any_item_type(current_type):
1565+
inherited_type = self._get_inherited_field_type(field_name, base_classes)
1566+
if inherited_type is None or not inherited_type.is_list or not inherited_type.data_types:
1567+
continue
1568+
1569+
new_type = inherited_type.model_copy(deep=True) if PYDANTIC_V2 else inherited_type.copy(deep=True)
1570+
1571+
# Preserve modifiers coming from the overriding schema.
1572+
if current_type is not None: # pragma: no branch
1573+
new_type.is_optional = new_type.is_optional or current_type.is_optional
1574+
new_type.is_dict = new_type.is_dict or current_type.is_dict
1575+
new_type.is_list = new_type.is_list or current_type.is_list
1576+
new_type.is_set = new_type.is_set or current_type.is_set
1577+
if new_type.kwargs is None and current_type.kwargs is not None: # pragma: no cover
1578+
new_type.kwargs = current_type.kwargs
1579+
1580+
# Some code paths represent the list type inside an outer container.
1581+
is_wrapped = (
1582+
current_type is not None
1583+
and not current_type.is_list
1584+
and len(current_type.data_types) == 1
1585+
and current_type.data_types[0].is_list
1586+
)
1587+
if is_wrapped:
1588+
wrapper = current_type.model_copy(deep=True) if PYDANTIC_V2 else current_type.copy(deep=True)
1589+
wrapper.data_types[0] = new_type
1590+
field.data_type = wrapper
1591+
continue
1592+
1593+
field.data_type = new_type # pragma: no cover
15421594
# ignore an undetected object
15431595
if ignore_duplicate_model and not fields and len(base_classes) == 1:
15441596
with self.model_resolver.current_base_path_context(self.model_resolver._base_path): # noqa: SLF001
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: allof_partial_override_array_items.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Thing(BaseModel):
13+
type: Optional[str] = 'playground:Thing'
14+
type_list: Optional[List[str]] = ['playground:Thing']
15+
16+
17+
class Person(Thing):
18+
type: Optional[str] = 'playground:Person'
19+
type_list: Optional[List[str]] = ['playground:Person']
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: allof_partial_override_array_items_no_parent.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Any, List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Thing(BaseModel):
13+
name: Optional[str] = None
14+
15+
16+
class Person(Thing):
17+
tags: Optional[List[Any]] = ['tag1']
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: allof_partial_override_deeply_nested_array.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Thing(BaseModel):
13+
cube: Optional[List[List[List[str]]]] = [[['a']]]
14+
15+
16+
class Person(Thing):
17+
cube: Optional[List[List[List[str]]]] = [[['b']]]
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: allof_partial_override_nested_array_items.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Thing(BaseModel):
13+
matrix: Optional[List[List[str]]] = [['a', 'b']]
14+
15+
16+
class Person(Thing):
17+
matrix: Optional[List[List[str]]] = [['c', 'd']]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# generated by datamodel-codegen:
2+
# filename: allof_partial_override_non_array_field.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, Field
10+
11+
12+
class Thing(BaseModel):
13+
name: Optional[str] = 'default_name'
14+
count: Optional[int] = 0
15+
16+
17+
class Person(Thing):
18+
name: Optional[str] = Field(None, title='Person name')
19+
count: Optional[int] = Field(None, description='Count value')
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: allof_partial_override_simple_list_any.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import List, Optional
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Parent(BaseModel):
13+
items: Optional[List[str]] = None
14+
15+
16+
class Child(Parent):
17+
items: Optional[List[str]] = None
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
openapi: "3.0.0"
2+
components:
3+
schemas:
4+
Thing:
5+
type: object
6+
properties:
7+
type:
8+
type: string
9+
default: playground:Thing
10+
type_list:
11+
type: array
12+
default:
13+
- playground:Thing
14+
items:
15+
type: string
16+
Person:
17+
allOf:
18+
- $ref: "#/components/schemas/Thing"
19+
- type: object
20+
properties:
21+
type:
22+
default: playground:Person
23+
type_list:
24+
default:
25+
- playground:Person
26+
items:
27+
title: A type entry
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
openapi: "3.0.0"
2+
components:
3+
schemas:
4+
Thing:
5+
type: object
6+
properties:
7+
name:
8+
type: string
9+
Person:
10+
allOf:
11+
- $ref: "#/components/schemas/Thing"
12+
- type: object
13+
properties:
14+
tags:
15+
type: array
16+
default:
17+
- tag1
18+
items:
19+
title: A tag entry
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
openapi: "3.0.0"
2+
components:
3+
schemas:
4+
Thing:
5+
type: object
6+
properties:
7+
cube:
8+
type: array
9+
default:
10+
- - - a
11+
items:
12+
type: array
13+
items:
14+
type: array
15+
items:
16+
type: string
17+
Person:
18+
allOf:
19+
- $ref: "#/components/schemas/Thing"
20+
- type: object
21+
properties:
22+
cube:
23+
default:
24+
- - - b
25+
items:
26+
title: A plane
27+
items:
28+
title: A row
29+
items:
30+
title: A cell

0 commit comments

Comments
 (0)