Skip to content

Commit 2aebb77

Browse files
authored
Handle types.UnionType in _serialize_python_type for Python 3.10-3.13 (#2848)
1 parent a2a4d74 commit 2aebb77

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

src/datamodel_code_generator/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,12 +632,29 @@ def _serialize_python_type(tp: type) -> str | None:
632632
633633
Returns None if the type doesn't need to be preserved (e.g., standard dict, list).
634634
"""
635+
import types # noqa: PLC0415
635636
from typing import get_args, get_origin # noqa: PLC0415
636637

637638
origin = get_origin(tp)
638639
args = get_args(tp)
639640
preserved_origins = _get_preserved_type_origins()
640641

642+
# Handle types.UnionType (X | Y syntax) in Python 3.10-3.13
643+
# In Python 3.10-3.13, get_origin(X | Y) returns types.UnionType which is distinct from typing.Union
644+
# In Python 3.14+, types.UnionType is the same as typing.Union, so this check is skipped
645+
from typing import Union # noqa: PLC0415
646+
647+
if (
648+
hasattr(types, "UnionType")
649+
and types.UnionType is not Union # Only applies to Python 3.10-3.13
650+
and origin is types.UnionType
651+
):
652+
if args:
653+
nested = [_serialize_python_type(a) for a in args]
654+
if any(n is not None for n in nested):
655+
return " | ".join(n or _simple_type_name(a) for n, a in zip(nested, args, strict=False))
656+
return None # pragma: no cover
657+
641658
if origin in preserved_origins:
642659
type_name = preserved_origins[origin]
643660
if args:

tests/data/python/input_model/pydantic_models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ModelWithPythonTypes(BaseModel):
3333
nested_in_list: list[Set[int]]
3434
optional_set: Optional[Set[str]]
3535
nullable_frozenset: Union[None, FrozenSet[str]]
36+
optional_mapping: Mapping[str, str] | None
3637

3738

3839
class RecursiveNode(BaseModel):

tests/test_input_model.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,17 @@ def test_input_model_union_none_frozenset(tmp_path: Path) -> None:
590590
output_path=tmp_path / "output.py",
591591
expected_output_contains=["frozenset[str] | None", "nullable_frozenset:"],
592592
)
593+
594+
595+
@SKIP_PYDANTIC_V1
596+
def test_input_model_optional_mapping_union_syntax(tmp_path: Path) -> None:
597+
"""Test that Mapping[str, str] | None using | syntax is preserved correctly.
598+
599+
In Python 3.10-3.13, get_origin(X | Y) returns types.UnionType.
600+
This test verifies that types.UnionType is handled correctly in those versions.
601+
"""
602+
run_input_model_and_assert(
603+
input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
604+
output_path=tmp_path / "output.py",
605+
expected_output_contains=["Mapping[str, str] | None", "optional_mapping:"],
606+
)

0 commit comments

Comments
 (0)