Skip to content

Commit a67163c

Browse files
authored
Fix x-python-type for Optional container types in anyOf schemas (#2840)
1 parent bb0eb62 commit a67163c

3 files changed

Lines changed: 61 additions & 4 deletions

File tree

src/datamodel_code_generator/parser/jsonschema.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,8 +1236,6 @@ class decorator which does not preserve staticmethod descriptors.
12361236
if not x_python_type or not isinstance(x_python_type, str):
12371237
return {}
12381238

1239-
base_type = x_python_type.split("[")[0].strip()
1240-
12411239
type_to_flag: dict[str, dict[str, bool]] = {
12421240
"Set": {"is_set": True},
12431241
"FrozenSet": {"is_frozen_set": True},
@@ -1249,7 +1247,34 @@ class decorator which does not preserve staticmethod descriptors.
12491247
"MutableSet": {"is_set": True},
12501248
}
12511249

1252-
return type_to_flag.get(base_type, {})
1250+
base_type = x_python_type.split("[")[0].strip()
1251+
if base_type in type_to_flag:
1252+
return type_to_flag[base_type]
1253+
1254+
if base_type in {"Union", "Optional"}:
1255+
bracket_start = x_python_type.find("[")
1256+
if bracket_start != -1:
1257+
inner = x_python_type[bracket_start + 1 : -1]
1258+
depth = 0
1259+
current = ""
1260+
for char in inner:
1261+
if char == "[":
1262+
depth += 1
1263+
elif char == "]":
1264+
depth -= 1
1265+
if char == "," and depth == 0:
1266+
arg_base = current.strip().split("[")[0]
1267+
if arg_base in type_to_flag:
1268+
return type_to_flag[arg_base]
1269+
current = ""
1270+
else:
1271+
current += char
1272+
if current.strip():
1273+
arg_base = current.strip().split("[")[0]
1274+
if arg_base in type_to_flag:
1275+
return type_to_flag[arg_base]
1276+
1277+
return {}
12531278

12541279
def _apply_title_as_name(self, name: str, obj: JsonSchemaObject) -> str:
12551280
"""Apply title as name if use_title_as_name is enabled."""

tests/data/python/input_model/pydantic_models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from collections.abc import Mapping, Sequence
6-
from typing import FrozenSet, Optional, Set
6+
from typing import FrozenSet, Optional, Set, Union
77

88
from pydantic import BaseModel
99

@@ -32,6 +32,7 @@ class ModelWithPythonTypes(BaseModel):
3232
tag_obj: Tag
3333
nested_in_list: list[Set[int]]
3434
optional_set: Optional[Set[str]]
35+
nullable_frozenset: Union[None, FrozenSet[str]]
3536

3637

3738
class RecursiveNode(BaseModel):

tests/test_input_model.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,3 +559,34 @@ def test_input_model_recursive_model_types(tmp_path: Path) -> None:
559559
output_path=tmp_path / "output.py",
560560
expected_output_contains=["set[str]", "value:"],
561561
)
562+
563+
564+
@SKIP_PYDANTIC_V1
565+
def test_input_model_optional_set_type(tmp_path: Path) -> None:
566+
"""Test that Optional[Set[str]] is preserved when converting Pydantic model."""
567+
run_input_model_and_assert(
568+
input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
569+
output_path=tmp_path / "output.py",
570+
expected_output_contains=["set[str] | None", "optional_set:"],
571+
)
572+
573+
574+
@SKIP_PYDANTIC_V1
575+
def test_input_model_optional_set_to_typeddict(tmp_path: Path) -> None:
576+
"""Test that Optional[Set[str]] works when outputting to TypedDict."""
577+
run_input_model_and_assert(
578+
input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
579+
output_path=tmp_path / "output.py",
580+
extra_args=["--output-model-type", "typing.TypedDict"],
581+
expected_output_contains=["TypedDict", "set[str] | None", "optional_set:"],
582+
)
583+
584+
585+
@SKIP_PYDANTIC_V1
586+
def test_input_model_union_none_frozenset(tmp_path: Path) -> None:
587+
"""Test that Union[None, FrozenSet[str]] is preserved (container not first arg)."""
588+
run_input_model_and_assert(
589+
input_model="tests.data.python.input_model.pydantic_models:ModelWithPythonTypes",
590+
output_path=tmp_path / "output.py",
591+
expected_output_contains=["frozenset[str] | None", "nullable_frozenset:"],
592+
)

0 commit comments

Comments
 (0)