Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 76 additions & 20 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,23 @@ def _get_enum_from_base(discriminator_model: DataModel, field_name: str) -> Enum
return None


def _get_single_discriminator_default(
data_type: DataType,
enum_source: Enum | None,
expected_value: DiscriminatorValue | None,
) -> DiscriminatorValue | Member | None:
"""Return the only valid discriminator default after resolving its type."""
if len(literals := data_type.literals) == 1:
return literals[0]
if (
len(data_type.enum_member_literals) != 1
or enum_source is None
or (member := enum_source.find_member(expected_value, coerce_strings=True)) is None
):
return None
return member


def _get_model_module_name(model: DataModel, model_path_to_module_name: Mapping[str, str]) -> str:
return model_path_to_module_name.get(model.path, model.module_name)

Expand Down Expand Up @@ -2042,6 +2059,23 @@ def _get_enum_discriminator_literal(enum_source: Enum, value: DiscriminatorValue
return member_value
return value

def __set_force_optional_discriminator_literal_default(
self,
model: DataModel,
discriminator_field: DataModelFieldBase,
literal: DiscriminatorValue | Member,
*,
can_retain_cache: bool,
) -> None:
"""Keep Pydantic v2 single-literal discriminator fields valid when forced optional."""
if not self.force_optional_for_required_fields or not _is_pydantic_v2_data_model_field(discriminator_field):
return

discriminator_field.default = literal
discriminator_field.required = False
discriminator_field.nullable = False
_clear_model_imports_cache_if_retained(model, can_retain_cache=can_retain_cache)

def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
self,
models: list[DataModel],
Expand Down Expand Up @@ -2127,24 +2161,31 @@ def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
for discriminator_field in discriminator_model.fields:
if field_name not in {discriminator_field.original_name, discriminator_field.name}:
continue
literals = discriminator_field.data_type.literals
const_value = discriminator_field.extras.get("const")
expected_value = discriminator_values[0] if discriminator_values else None

# Check if literals match (existing behavior)
literals_match = len(literals) == 1 and literals[0] == expected_value
# Check if const value matches (for msgspec with type: string + const)
const_match = const_value is not None and const_value == expected_value

if literals_match:
if (
len(literals := discriminator_field.data_type.literals) == 1
and (literal := literals[0]) == expected_value
):
has_one_literal = True
if _is_msgspec_struct(discriminator_model): # pragma: no cover
_add_msgspec_base_class_kwarg(discriminator_model, "tag_field", f"'{field_name}'")
_add_msgspec_base_class_kwarg(discriminator_model, "tag", repr(expected_value))
discriminator_field.extras["is_classvar"] = True
_clear_model_imports_cache_if_retained(
discriminator_model, can_retain_cache=can_retain_cache
)
match discriminator_model:
case _ if _is_msgspec_struct(discriminator_model): # pragma: no cover
_add_msgspec_base_class_kwarg(discriminator_model, "tag_field", f"'{field_name}'")
_add_msgspec_base_class_kwarg(discriminator_model, "tag", repr(expected_value))
discriminator_field.extras["is_classvar"] = True
_clear_model_imports_cache_if_retained(
discriminator_model, can_retain_cache=can_retain_cache
)
case _:
self.__set_force_optional_discriminator_literal_default(
discriminator_model,
discriminator_field,
literal,
can_retain_cache=can_retain_cache,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# Found the discriminator field, no need to keep looking
break

Expand All @@ -2167,17 +2208,32 @@ def __apply_discriminator_type( # noqa: PLR0912, PLR0914, PLR0915
if field_data_type.reference: # pragma: no cover
self.generation_store.detach_data_type_ref(field_data_type)

self.generation_store.replace_field_type(
discriminator_field,
self._create_discriminator_data_type(
enum_source,
discriminator_values,
discriminator_model,
imports,
),
new_discriminator_data_type = self._create_discriminator_data_type(
enum_source,
discriminator_values,
discriminator_model,
imports,
)
self.generation_store.replace_field_type(discriminator_field, new_discriminator_data_type)
discriminator_field.data_type.parent = discriminator_field
discriminator_field.required = True
if (
self.force_optional_for_required_fields
and (
literal_default := _get_single_discriminator_default(
new_discriminator_data_type,
enum_source,
expected_value,
)
)
is not None
):
self.__set_force_optional_discriminator_literal_default(
discriminator_model,
discriminator_field,
literal_default,
can_retain_cache=can_retain_cache,
)
imports.append(discriminator_field.imports)
has_one_literal = True
if not has_one_literal:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"CardPayment": {
"kind": "card",
"card_number": null
},
"CashPayment": {
"kind": "cash",
"received_amount": null
},
"Payment": {
"kind": "cash",
"received_amount": 50
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# generated by datamodel-codegen:
# filename: discriminator_enum_single_value_anyof.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import Enum
from typing import Literal

from pydantic import BaseModel, Field, RootModel


class ToolType(Enum):
function = 'function'


class FunctionToolCall(BaseModel):
id: str | None = None
type: Literal[ToolType.function] = ToolType.function


class CustomToolCall(BaseModel):
type: Literal['CustomToolCall'] = 'CustomToolCall'


class ToolCallUnion(RootModel[FunctionToolCall | CustomToolCall | None]):
root: FunctionToolCall | CustomToolCall | None = Field(None, discriminator='type')
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# generated by datamodel-codegen:
# filename: discriminator_force_optional.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field, RootModel


class CardPayment(BaseModel):
kind: Literal['card'] = 'card'
card_number: int | None = None


class CashPayment(BaseModel):
kind: Literal['cash'] = 'cash'
received_amount: int | None = None


class Payment(RootModel[CardPayment | CashPayment | None]):
root: CardPayment | CashPayment | None = Field(None, discriminator='kind')
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# generated by datamodel-codegen:
# filename: discriminator_force_optional.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Annotated, Literal

from pydantic import Field
from pydantic.dataclasses import dataclass
from typing_extensions import TypeAliasType


@dataclass
class CardPayment:
kind: Literal['card'] = 'card'
card_number: int | None = None


@dataclass
class CashPayment:
kind: Literal['cash'] = 'cash'
received_amount: int | None = None


Payment = TypeAliasType(
"Payment",
Annotated[
CardPayment | CashPayment | None, Field(None, discriminator='kind')
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# generated by datamodel-codegen:
# filename: discriminator_integer_mapping.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from enum import IntEnum
from typing import Literal

from pydantic import BaseModel, Field, RootModel


class Kind(IntEnum):
integer_1 = 1


class Foo(BaseModel):
kind: Literal[Kind.integer_1] = Kind.integer_1


class Kind1(IntEnum):
integer_2 = 2
integer_3 = 3


class Bar(BaseModel):
kind: Literal[Kind1.integer_2, Kind1.integer_3]


class Base(RootModel[Foo | Bar | None]):
root: Foo | Bar | None = Field(None, discriminator='kind')
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# generated by datamodel-codegen:
# filename: discriminator_force_optional.yaml
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field, RootModel


class CardPayment(BaseModel):
kind: Literal['card']
card_number: int


class CashPayment(BaseModel):
kind: Literal['cash']
received_amount: int


class Payment(RootModel[CardPayment | CashPayment]):
root: CardPayment | CashPayment = Field(..., discriminator='kind')
37 changes: 37 additions & 0 deletions tests/data/openapi/discriminator_force_optional.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
openapi: 3.1.0
info:
title: Force Optional Discriminator
version: "0.1.0"
paths: {}
components:
schemas:
CardPayment:
type: object
required:
- kind
- card_number
properties:
kind:
type: string
const: card
card_number:
type: integer
CashPayment:
type: object
required:
- kind
- received_amount
properties:
kind:
type: string
received_amount:
type: integer
Payment:
oneOf:
- $ref: "#/components/schemas/CardPayment"
- $ref: "#/components/schemas/CashPayment"
discriminator:
propertyName: kind
mapping:
card: "#/components/schemas/CardPayment"
cash: "#/components/schemas/CashPayment"
Loading
Loading