Skip to content

Commit 3a53bf3

Browse files
committed
Refine discriminator enum mapping
1 parent 462abc2 commit 3a53bf3

5 files changed

Lines changed: 182 additions & 38 deletions

File tree

src/datamodel_code_generator/parser/base.py

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,7 +1564,7 @@ def _create_discriminator_data_type(
15641564
if enum_source:
15651565
if self.use_enum_values_in_discriminator:
15661566
enum_class_name = enum_source.reference.short_name
1567-
enum_member_literals: list[tuple[str, DiscriminatorValue]] = []
1567+
enum_member_literals: list[tuple[str, str]] = []
15681568
for value in discriminator_values:
15691569
member = enum_source.find_member(value)
15701570
if member and member.field.name:
@@ -1575,24 +1575,28 @@ def _create_discriminator_data_type(
15751575
if enum_source.module_path != discriminator_model.module_path: # pragma: no cover
15761576
imports.append(Import.from_full_path(enum_source.name))
15771577
else:
1578-
# According to OpenAPI specification, mapping discriminators are always string values.
1579-
# However, if the mapped object is an enum, we want to use the real enum value instead of
1580-
# the string value.
1581-
# See: https://swagger.io/specification/#options-for-mapping-values-to-schemas
1582-
# Fix: https://github.com/koxudaxi/datamodel-code-generator/issues/3073
1583-
for i, value in enumerate(discriminator_values):
1584-
if member := enum_source.find_member(value):
1585-
match member.field.default:
1586-
case str():
1587-
discriminator_values[i] = member.field.default.strip("'\"")
1588-
case _ if isinstance(member.field.default, DiscriminatorValue):
1589-
discriminator_values[i] = member.field.default
1590-
1591-
data_type = self.data_type(literals=discriminator_values)
1578+
data_type = self.data_type(
1579+
literals=[
1580+
Parser._get_enum_discriminator_literal(enum_source, value) for value in discriminator_values
1581+
]
1582+
)
15921583
else:
15931584
data_type = self.data_type(literals=discriminator_values)
15941585
return data_type
15951586

1587+
@staticmethod
1588+
def _get_enum_discriminator_literal(enum_source: Enum, value: DiscriminatorValue) -> DiscriminatorValue:
1589+
member = enum_source.find_member(value)
1590+
if not member:
1591+
return value
1592+
1593+
default = member.field.default
1594+
if isinstance(default, str):
1595+
return default.strip("'\"")
1596+
if isinstance(default, int | bool):
1597+
return default
1598+
return value
1599+
15961600
def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
15971601
self,
15981602
models: list[DataModel],
@@ -1706,28 +1710,29 @@ def get_discriminator_field_value(
17061710
msg = f"Discriminator type is not found. {data_type.reference.path}"
17071711
raise RuntimeError(msg)
17081712

1709-
enum_from_base: Enum | None = None
1710-
for base_class in discriminator_model.base_classes:
1711-
if not base_class.reference or not base_class.reference.source: # pragma: no cover
1712-
continue
1713-
base_model = base_class.reference.source
1714-
if not isinstance( # pragma: no cover
1715-
base_model,
1716-
(
1717-
pydantic_model_v2.BaseModel,
1718-
dataclass_model.DataClass,
1719-
msgspec_model.Struct,
1720-
),
1721-
):
1722-
continue
1723-
for base_field in base_model.fields: # pragma: no branch
1724-
if field_name not in {base_field.original_name, base_field.name}: # pragma: no cover
1713+
def get_enum_from_base(
1714+
discriminator_model: DataModel = discriminator_model,
1715+
field_name: str = field_name,
1716+
) -> Enum | None:
1717+
for base_class in discriminator_model.base_classes:
1718+
if not base_class.reference or not base_class.reference.source: # pragma: no cover
17251719
continue
1726-
enum_from_base = base_field.data_type.find_source(Enum)
1727-
if enum_from_base: # pragma: no branch
1728-
break
1729-
if enum_from_base: # pragma: no branch
1730-
break
1720+
base_model = base_class.reference.source
1721+
if not isinstance( # pragma: no cover
1722+
base_model,
1723+
(
1724+
pydantic_model_v2.BaseModel,
1725+
dataclass_model.DataClass,
1726+
msgspec_model.Struct,
1727+
),
1728+
):
1729+
continue
1730+
for base_field in base_model.fields: # pragma: no branch
1731+
if field_name not in {base_field.original_name, base_field.name}: # pragma: no cover
1732+
continue
1733+
if enum_from_base := base_field.data_type.find_source(Enum): # pragma: no branch
1734+
return enum_from_base
1735+
return None
17311736

17321737
has_one_literal = False
17331738
for discriminator_field in discriminator_model.fields:
@@ -1759,7 +1764,9 @@ def get_discriminator_field_value(
17591764
discriminator_field.extras["is_classvar"] = True
17601765
break
17611766

1762-
enum_source = discriminator_field.data_type.find_source(Enum) or enum_from_base
1767+
enum_source = discriminator_field.data_type.find_source(Enum)
1768+
if self.use_enum_values_in_discriminator:
1769+
enum_source = enum_source or get_enum_from_base()
17631770

17641771
for field_data_type in discriminator_field.data_type.all_data_types:
17651772
if field_data_type.reference: # pragma: no cover
@@ -1774,7 +1781,7 @@ def get_discriminator_field_value(
17741781
has_one_literal = True
17751782
if not has_one_literal:
17761783
new_data_type = self._create_discriminator_data_type(
1777-
enum_from_base, discriminator_values, discriminator_model, imports
1784+
get_enum_from_base(), discriminator_values, discriminator_model, imports
17781785
)
17791786
# Handle multiple aliases (Pydantic v2 AliasChoices)
17801787
single_alias: str | None = None
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# generated by datamodel-codegen:
2+
# filename: discriminator_float_mapping.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
9+
10+
from pydantic import BaseModel, Field, RootModel
11+
12+
13+
class Kind(Enum):
14+
number_1_5 = 1.5
15+
number_2_5 = 2.5
16+
17+
18+
class Foo(BaseModel):
19+
kind: Literal['1.5']
20+
21+
22+
class Kind1(Enum):
23+
number_3_5 = 3.5
24+
number_4_5 = 4.5
25+
26+
27+
class Bar(BaseModel):
28+
kind: Literal['3.5']
29+
30+
31+
class Base(RootModel[Foo | Bar]):
32+
root: Foo | Bar = Field(..., discriminator='kind')
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: discriminator_integer_mapping.yaml
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from enum import IntEnum
8+
from typing import Literal
9+
10+
from pydantic import BaseModel, Field, RootModel
11+
12+
13+
class Kind(IntEnum):
14+
integer_1 = 1
15+
16+
17+
class Foo(BaseModel):
18+
kind: Literal[Kind.integer_1]
19+
20+
21+
class Kind1(IntEnum):
22+
integer_2 = 2
23+
integer_3 = 3
24+
25+
26+
class Bar(BaseModel):
27+
kind: Literal[Kind1.integer_2, Kind1.integer_3]
28+
29+
30+
class Base(RootModel[Foo | Bar]):
31+
root: Foo | Bar = Field(..., discriminator='kind')
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
openapi: 3.0.3
2+
info:
3+
title: Float Discriminator Mapping
4+
version: 1.0.0
5+
paths: {}
6+
components:
7+
schemas:
8+
Base:
9+
oneOf:
10+
- $ref: '#/components/schemas/Foo'
11+
- $ref: '#/components/schemas/Bar'
12+
discriminator:
13+
propertyName: kind
14+
mapping:
15+
'1.5': '#/components/schemas/Foo'
16+
'3.5': '#/components/schemas/Bar'
17+
Foo:
18+
type: object
19+
properties:
20+
kind:
21+
type: number
22+
enum:
23+
- 1.5
24+
- 2.5
25+
required:
26+
- kind
27+
Bar:
28+
type: object
29+
properties:
30+
kind:
31+
type: number
32+
enum:
33+
- 3.5
34+
- 4.5
35+
required:
36+
- kind

tests/main/openapi/test_main_openapi.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,44 @@ def test_main_openapi_discriminator_integer_mapping(output_file: Path) -> None:
178178
)
179179

180180

181+
@pytest.mark.skipif(
182+
black.__version__.split(".")[0] == "19",
183+
reason="Installed black doesn't support the old style",
184+
)
185+
def test_main_openapi_discriminator_integer_mapping_use_enum(output_file: Path) -> None:
186+
"""Integer discriminator mapping preserves enum member literal values."""
187+
run_main_and_assert(
188+
input_path=OPEN_API_DATA_PATH / "discriminator_integer_mapping.yaml",
189+
output_path=output_file,
190+
input_file_type="openapi",
191+
assert_func=assert_file_content,
192+
expected_file=EXPECTED_OPENAPI_PATH / "discriminator" / "integer_mapping_use_enum.py",
193+
extra_args=[
194+
"--target-python-version",
195+
"3.10",
196+
"--output-model-type",
197+
"pydantic_v2.BaseModel",
198+
"--use-enum-values-in-discriminator",
199+
],
200+
)
201+
202+
203+
@pytest.mark.skipif(
204+
black.__version__.split(".")[0] == "19",
205+
reason="Installed black doesn't support the old style",
206+
)
207+
def test_main_openapi_discriminator_float_mapping(output_file: Path) -> None:
208+
"""Unsupported discriminator enum values keep their mapping string values."""
209+
run_main_and_assert(
210+
input_path=OPEN_API_DATA_PATH / "discriminator_float_mapping.yaml",
211+
output_path=output_file,
212+
input_file_type="openapi",
213+
assert_func=assert_file_content,
214+
expected_file=EXPECTED_OPENAPI_PATH / "discriminator" / "float_mapping.py",
215+
extra_args=["--target-python-version", "3.10", "--output-model-type", "pydantic_v2.BaseModel"],
216+
)
217+
218+
181219
@pytest.mark.skipif(
182220
black.__version__.split(".")[0] == "19",
183221
reason="Installed black doesn't support the old style",

0 commit comments

Comments
 (0)