Skip to content
Merged
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
81 changes: 80 additions & 1 deletion src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,14 @@ def type_has_null(self) -> bool:
"""Check if the type list contains null."""
return isinstance(self.type, list) and "null" in self.type

@cached_property
def has_multiple_types(self) -> bool:
"""Check if the type is a list with multiple non-null types."""
if not isinstance(self.type, list):
return False
non_null_types = [t for t in self.type if t != "null"]
return len(non_null_types) > 1


@lru_cache
def get_ref_type(ref: str) -> JSONReference:
Expand Down Expand Up @@ -1301,6 +1309,17 @@ def parse_item( # noqa: PLR0911, PLR0912
if item.is_object or item.patternProperties:
object_path = get_special_path("object", path)
if item.properties:
if item.has_multiple_types and isinstance(item.type, list):
data_types: list[DataType] = []
data_types.append(self.parse_object(name, item, object_path, singular_name=singular_name))
data_types.extend(
self.data_type_manager.get_data_type(
self._get_type_with_mappings(t, item.format or "default"),
)
for t in item.type
if t not in {"object", "null"}
)
return self.data_type(data_types=data_types)
return self.parse_object(name, item, object_path, singular_name=singular_name)
if item.patternProperties:
# support only single key dict.
Expand Down Expand Up @@ -1535,6 +1554,63 @@ def parse_root_type( # noqa: PLR0912
self.results.append(data_model_root_type)
return self.data_type(reference=reference)

def _parse_multiple_types_with_properties(
self,
name: str,
obj: JsonSchemaObject,
type_list: list[str],
path: list[str],
) -> None:
"""Parse a schema with multiple types including object with properties."""
data_types: list[DataType] = []

object_path = get_special_path("object", path)
object_data_type = self.parse_object(name, obj, object_path)
data_types.append(object_data_type)

data_types.extend(
self.data_type_manager.get_data_type(
self._get_type_with_mappings(t, obj.format or "default"),
)
for t in type_list
if t not in {"object", "null"}
)

is_nullable = obj.nullable or obj.type_has_null
required = not is_nullable and not (obj.has_default and self.apply_default_values_for_required_fields)

reference = self.model_resolver.add(path, name, loaded=True, class_name=True)
self.set_title(reference.path, obj)
self.set_additional_properties(reference.path, obj)

data_model_root_type = self.data_model_root_type(
reference=reference,
fields=[
self.data_model_field_type(
data_type=self.data_type(data_types=data_types),
default=obj.default,
required=required,
constraints=obj.dict() if self.field_constraints else {},
nullable=obj.type_has_null if self.strict_nullable else None,
strip_default_none=self.strip_default_none,
extras=self.get_field_extras(obj),
use_annotated=self.use_annotated,
use_field_description=self.use_field_description,
use_inline_field_description=self.use_inline_field_description,
original_name=None,
has_default=obj.has_default,
)
],
custom_base_class=obj.custom_base_path or self.base_class,
custom_template_dir=self.custom_template_dir,
extra_template_data=self.extra_template_data,
path=self.current_source_path,
nullable=obj.type_has_null,
treat_dot_as_module=self.treat_dot_as_module,
default=obj.default if obj.has_default else UNDEFINED,
)
self.results.append(data_model_root_type)

def parse_enum_as_literal(self, obj: JsonSchemaObject) -> DataType:
"""Parse enum values as a Literal type."""
return self.data_type(literals=[i for i in obj.enum if i is not None])
Expand Down Expand Up @@ -1843,7 +1919,10 @@ def parse_obj(
if isinstance(data_type, EmptyDataType) and obj.properties:
self.parse_object(name, obj, path) # pragma: no cover
elif obj.properties:
self.parse_object(name, obj, path)
if obj.has_multiple_types and isinstance(obj.type, list):
self._parse_multiple_types_with_properties(name, obj, obj.type, path)
else:
self.parse_object(name, obj, path)
elif obj.patternProperties:
self.parse_root_type(name, obj, path)
elif obj.type == "object":
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# generated by datamodel-codegen:
# filename: multiple_types_with_object.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Optional, Union

from pydantic import BaseModel


class External(BaseModel):
name: Optional[str] = None


class Config(BaseModel):
value: Optional[int] = None


class TopLevelMultiType1(BaseModel):
enabled: Optional[bool] = None


class TopLevelMultiType(BaseModel):
__root__: Union[TopLevelMultiType1, bool]


class Model(BaseModel):
external: Optional[Union[External, bool]] = None
config: Optional[Union[Optional[Config], str]] = None
top_level_ref: Optional[TopLevelMultiType] = None
35 changes: 35 additions & 0 deletions tests/data/jsonschema/multiple_types_with_object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"TopLevelMultiType": {
"type": ["boolean", "object"],
"properties": {
"enabled": {
"type": "boolean"
}
}
}
},
"type": "object",
"properties": {
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {
"type": "string"
}
}
},
"config": {
"type": ["null", "string", "object"],
"properties": {
"value": {
"type": "integer"
}
}
},
"top_level_ref": {
"$ref": "#/definitions/TopLevelMultiType"
}
}
}
10 changes: 10 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3000,6 +3000,16 @@ def test_main_jsonschema_empty_items_array(output_file: Path) -> None:
)


def test_main_jsonschema_multiple_types_with_object(output_file: Path) -> None:
"""Test multiple types in array including object with properties generates Union type."""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "multiple_types_with_object.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
)


@MSGSPEC_LEGACY_BLACK_SKIP
def test_main_jsonschema_type_alias_with_circular_ref_to_class_msgspec(min_version: str, output_file: Path) -> None:
"""Test TypeAlias with circular reference to class generates quoted forward refs."""
Expand Down
Loading