Skip to content

Commit 39a966f

Browse files
authored
Fix invalid Union syntax when using --collapse-root-models (#2594)
* Add support for collapsing root models with empty unions to generate Any type * Refactor import handling to correctly manage optional types in type hints * Refactor type imports for improved clarity and organization in type hints
1 parent 9445b94 commit 39a966f

6 files changed

Lines changed: 74 additions & 20 deletions

File tree

src/datamodel_code_generator/model/base.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from datamodel_code_generator.types import (
2929
ANY,
3030
NONE,
31+
OPTIONAL_PREFIX,
3132
UNION_PREFIX,
3233
DataType,
3334
Nullable,
@@ -188,16 +189,16 @@ def imports(self) -> tuple[Import, ...]:
188189
"""Get all imports required for this field's type hint."""
189190
type_hint = self.type_hint
190191
has_union = not self.data_type.use_union_operator and UNION_PREFIX in type_hint
192+
has_optional = OPTIONAL_PREFIX in type_hint
191193
imports: list[tuple[Import] | Iterator[Import]] = [
192-
iter(i for i in self.data_type.all_imports if not (not has_union and i == IMPORT_UNION))
194+
iter(
195+
i
196+
for i in self.data_type.all_imports
197+
if not ((not has_union and i == IMPORT_UNION) or (not has_optional and i == IMPORT_OPTIONAL))
198+
)
193199
]
194200

195-
if self.fall_back_to_nullable:
196-
if (
197-
self.nullable or (self.nullable is None and not self.required)
198-
) and not self.data_type.use_union_operator:
199-
imports.append((IMPORT_OPTIONAL,))
200-
elif self.nullable and not self.data_type.use_union_operator: # pragma: no cover
201+
if has_optional:
201202
imports.append((IMPORT_OPTIONAL,))
202203
if self.use_annotated and self.annotated:
203204
imports.append((IMPORT_ANNOTATED,))

src/datamodel_code_generator/parser/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from datamodel_code_generator.imports import (
3939
IMPORT_ANNOTATIONS,
4040
IMPORT_LITERAL,
41+
IMPORT_OPTIONAL,
4142
Import,
4243
Imports,
4344
)
@@ -1284,7 +1285,7 @@ def __collapse_root_models( # noqa: PLR0912
12841285
original_field = get_most_of_parent(data_type, DataModelFieldBase)
12851286
if original_field: # pragma: no cover
12861287
# TODO: Improve detection of reference type
1287-
imports.append(original_field.imports)
1288+
imports.append([i for i in original_field.imports if i != IMPORT_OPTIONAL])
12881289

12891290
data_type.remove_reference()
12901291

src/datamodel_code_generator/types.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
IMPORT_ABC_MAPPING,
3939
IMPORT_ABC_SEQUENCE,
4040
IMPORT_ABC_SET,
41+
IMPORT_ANY,
4142
IMPORT_DICT,
4243
IMPORT_FROZEN_SET,
4344
IMPORT_LIST,
@@ -52,16 +53,6 @@
5253
from datamodel_code_generator.reference import Reference, _BaseModel
5354
from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict
5455

55-
if TYPE_CHECKING:
56-
import builtins
57-
from collections.abc import Iterable, Iterator, Sequence
58-
59-
from datamodel_code_generator.model.base import DataModelFieldBase
60-
61-
if PYDANTIC_V2:
62-
from pydantic import GetCoreSchemaHandler
63-
from pydantic_core import core_schema
64-
6556
T = TypeVar("T")
6657

6758
OPTIONAL = "Optional"
@@ -90,6 +81,16 @@
9081
NOT_REQUIRED = "NotRequired"
9182
NOT_REQUIRED_PREFIX = f"{NOT_REQUIRED}["
9283

84+
if TYPE_CHECKING:
85+
import builtins
86+
from collections.abc import Iterable, Iterator, Sequence
87+
88+
from datamodel_code_generator.model.base import DataModelFieldBase
89+
90+
if PYDANTIC_V2:
91+
from pydantic import GetCoreSchemaHandler
92+
from pydantic_core import core_schema
93+
9394

9495
class StrictTypes(Enum):
9596
"""Strict type options for generated models."""
@@ -486,7 +487,7 @@ def type_hint(self) -> str: # noqa: PLR0912, PLR0915
486487
data_types: list[str] = []
487488
for data_type in self.data_types:
488489
data_type_type = data_type.type_hint
489-
if data_type_type in data_types: # pragma: no cover
490+
if not data_type_type or data_type_type in data_types:
490491
continue
491492

492493
if data_type_type == NONE:
@@ -501,7 +502,10 @@ def type_hint(self) -> str: # noqa: PLR0912, PLR0915
501502
self.is_optional = True
502503

503504
data_types.append(non_optional_data_type_type)
504-
if len(data_types) == 1:
505+
if not data_types:
506+
type_ = ANY
507+
self.import_ = self.import_ or IMPORT_ANY
508+
elif len(data_types) == 1:
505509
type_ = data_types[0]
506510
elif self.use_union_operator:
507511
type_ = UNION_OPERATOR_DELIMITER.join(data_types)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: collapse_root_models_empty_union.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Any
8+
9+
from pydantic import BaseModel
10+
11+
12+
class Model(BaseModel):
13+
field: Any = None
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"field": {
6+
"anyOf": [
7+
{"$ref": "#/$defs/NullType1"},
8+
{"$ref": "#/$defs/NullType2"}
9+
]
10+
}
11+
},
12+
"$defs": {
13+
"NullType1": {
14+
"type": "null"
15+
},
16+
"NullType2": {
17+
"type": "null"
18+
}
19+
}
20+
}

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,3 +2975,18 @@ def test_main_jsonschema_empty_items_array(output_file: Path) -> None:
29752975
input_file_type="jsonschema",
29762976
assert_func=assert_file_content,
29772977
)
2978+
2979+
2980+
def test_main_jsonschema_collapse_root_models_empty_union(output_file: Path) -> None:
2981+
"""Test that collapse-root-models with empty union fallback generates Any instead of invalid Union syntax.
2982+
2983+
This test covers the fix for issue #2161 where --collapse-root-models could generate
2984+
invalid Python syntax like Union[, str] when all union members are filtered out.
2985+
"""
2986+
run_main_and_assert(
2987+
input_path=JSON_SCHEMA_DATA_PATH / "collapse_root_models_empty_union.json",
2988+
output_path=output_file,
2989+
input_file_type="jsonschema",
2990+
assert_func=assert_file_content,
2991+
extra_args=["--collapse-root-models"],
2992+
)

0 commit comments

Comments
 (0)