Skip to content

Commit ed18dc9

Browse files
authored
Fix: Enhance handling of allOf with oneOf and anyOf references in OpenAPI generation (#2605)
1 parent fb3a4b6 commit ed18dc9

6 files changed

Lines changed: 273 additions & 3 deletions

File tree

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,9 +1159,22 @@ def _parse_all_of_item( # noqa: PLR0913, PLR0917
11591159
) -> None:
11601160
for all_of_item in obj.allOf:
11611161
if all_of_item.ref: # $ref
1162-
ref = self.model_resolver.add_ref(all_of_item.ref)
1163-
if ref.path not in {b.path for b in base_classes}:
1164-
base_classes.append(ref)
1162+
ref_schema = self._load_ref_schema_object(all_of_item.ref)
1163+
1164+
if ref_schema.oneOf or ref_schema.anyOf:
1165+
self.model_resolver.add(path, name, class_name=True, loaded=True)
1166+
if ref_schema.anyOf:
1167+
union_models.extend(
1168+
d.reference for d in self.parse_any_of(name, ref_schema, path) if d.reference
1169+
)
1170+
if ref_schema.oneOf:
1171+
union_models.extend(
1172+
d.reference for d in self.parse_one_of(name, ref_schema, path) if d.reference
1173+
)
1174+
else:
1175+
ref = self.model_resolver.add_ref(all_of_item.ref)
1176+
if ref.path not in {b.path for b in base_classes}:
1177+
base_classes.append(ref)
11651178
else:
11661179
module_name = get_module_name(name, None, treat_dot_as_module=self.treat_dot_as_module)
11671180
object_fields = self.parse_object_fields(
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_with_anyof_ref.yaml
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 Union
9+
10+
from pydantic import BaseModel, RootModel
11+
12+
13+
class ItemType(Enum):
14+
text = 'text'
15+
16+
17+
class TextItem(BaseModel):
18+
itemType: ItemType
19+
text: str
20+
21+
22+
class ItemType1(Enum):
23+
number = 'number'
24+
25+
26+
class NumberItem(BaseModel):
27+
itemType: ItemType1
28+
value: int
29+
30+
31+
class Item(RootModel[Union[TextItem, NumberItem]]):
32+
root: Union[TextItem, NumberItem]
33+
34+
35+
class ItemPostRequest1(BaseModel):
36+
itemId: str
37+
38+
39+
class ItemPostRequest2(TextItem, ItemPostRequest1):
40+
pass
41+
42+
43+
class ItemPostRequest3(NumberItem, ItemPostRequest1):
44+
pass
45+
46+
47+
class ItemPostRequest(RootModel[Union[ItemPostRequest2, ItemPostRequest3]]):
48+
root: Union[ItemPostRequest2, ItemPostRequest3]
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_with_oneof_ref.yaml
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 Literal, Union
9+
10+
from pydantic import BaseModel, Field, RootModel
11+
12+
13+
class UserType(Enum):
14+
admin = 'admin'
15+
16+
17+
class AdminUser(BaseModel):
18+
userType: Literal['admin']
19+
adminLevel: int
20+
21+
22+
class UserType1(Enum):
23+
regular = 'regular'
24+
25+
26+
class RegularUser(BaseModel):
27+
userType: Literal['regular']
28+
username: str
29+
30+
31+
class User(RootModel[Union[AdminUser, RegularUser]]):
32+
root: Union[AdminUser, RegularUser] = Field(..., discriminator='userType')
33+
34+
35+
class UserPostRequest1(BaseModel):
36+
userId: str
37+
38+
39+
class UserPostRequest2(AdminUser, UserPostRequest1):
40+
pass
41+
42+
43+
class UserPostRequest3(RegularUser, UserPostRequest1):
44+
pass
45+
46+
47+
class UserPostRequest(RootModel[Union[UserPostRequest2, UserPostRequest3]]):
48+
root: Union[UserPostRequest2, UserPostRequest3]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Test case for allOf referencing a schema with anyOf
2+
# This tests the anyOf branch in _parse_all_of_item
3+
4+
openapi: 3.0.0
5+
info:
6+
title: Test API
7+
version: 1.0.0
8+
paths:
9+
/items:
10+
post:
11+
summary: Create an item
12+
requestBody:
13+
required: true
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/ItemPostRequest'
18+
responses:
19+
'200':
20+
description: Success
21+
components:
22+
schemas:
23+
TextItem:
24+
type: object
25+
required:
26+
- itemType
27+
- text
28+
properties:
29+
itemType:
30+
type: string
31+
enum:
32+
- text
33+
text:
34+
type: string
35+
NumberItem:
36+
type: object
37+
required:
38+
- itemType
39+
- value
40+
properties:
41+
itemType:
42+
type: string
43+
enum:
44+
- number
45+
value:
46+
type: integer
47+
Item:
48+
anyOf:
49+
- $ref: '#/components/schemas/TextItem'
50+
- $ref: '#/components/schemas/NumberItem'
51+
ItemPostRequest:
52+
allOf:
53+
- $ref: '#/components/schemas/Item'
54+
- type: object
55+
required:
56+
- itemId
57+
properties:
58+
itemId:
59+
type: string
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Test case for issue #1763
2+
# allOf referencing a schema with oneOf + discriminator
3+
4+
openapi: 3.0.0
5+
info:
6+
title: Test API
7+
version: 1.0.0
8+
paths:
9+
/users:
10+
post:
11+
summary: Create a user
12+
requestBody:
13+
required: true
14+
content:
15+
application/json:
16+
schema:
17+
$ref: '#/components/schemas/UserPostRequest'
18+
responses:
19+
'200':
20+
description: Success
21+
components:
22+
schemas:
23+
AdminUser:
24+
type: object
25+
required:
26+
- userType
27+
- adminLevel
28+
properties:
29+
userType:
30+
type: string
31+
enum:
32+
- admin
33+
adminLevel:
34+
type: integer
35+
RegularUser:
36+
type: object
37+
required:
38+
- userType
39+
- username
40+
properties:
41+
userType:
42+
type: string
43+
enum:
44+
- regular
45+
username:
46+
type: string
47+
User:
48+
oneOf:
49+
- $ref: '#/components/schemas/AdminUser'
50+
- $ref: '#/components/schemas/RegularUser'
51+
discriminator:
52+
propertyName: userType
53+
mapping:
54+
admin: '#/components/schemas/AdminUser'
55+
regular: '#/components/schemas/RegularUser'
56+
UserPostRequest:
57+
allOf:
58+
- $ref: '#/components/schemas/User'
59+
- type: object
60+
required:
61+
- userId
62+
properties:
63+
userId:
64+
type: string

tests/main/openapi/test_main_openapi.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,44 @@ def test_main_openapi_discriminator_allof_no_subtypes(output_file: Path) -> None
135135
)
136136

137137

138+
def test_main_openapi_allof_with_oneof_ref(output_file: Path) -> None:
139+
"""Test OpenAPI generation with allOf referencing a oneOf schema.
140+
141+
This tests the case where allOf combines a $ref to a schema with oneOf/discriminator
142+
and additional properties. Regression test for issue #1763.
143+
"""
144+
run_main_and_assert(
145+
input_path=OPEN_API_DATA_PATH / "allof_with_oneof_ref.yaml",
146+
output_path=output_file,
147+
input_file_type="openapi",
148+
assert_func=assert_file_content,
149+
expected_file=EXPECTED_OPENAPI_PATH / "allof_with_oneof_ref.py",
150+
extra_args=[
151+
"--output-model-type",
152+
"pydantic_v2.BaseModel",
153+
],
154+
)
155+
156+
157+
def test_main_openapi_allof_with_anyof_ref(output_file: Path) -> None:
158+
"""Test OpenAPI generation with allOf referencing an anyOf schema.
159+
160+
This tests the case where allOf combines a $ref to a schema with anyOf
161+
and additional properties.
162+
"""
163+
run_main_and_assert(
164+
input_path=OPEN_API_DATA_PATH / "allof_with_anyof_ref.yaml",
165+
output_path=output_file,
166+
input_file_type="openapi",
167+
assert_func=assert_file_content,
168+
expected_file=EXPECTED_OPENAPI_PATH / "allof_with_anyof_ref.py",
169+
extra_args=[
170+
"--output-model-type",
171+
"pydantic_v2.BaseModel",
172+
],
173+
)
174+
175+
138176
def test_main_pydantic_basemodel(output_file: Path) -> None:
139177
"""Test OpenAPI generation with Pydantic BaseModel output."""
140178
run_main_and_assert(

0 commit comments

Comments
 (0)