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
1 change: 1 addition & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ class DataModelType(Enum):

PydanticBaseModel = "pydantic.BaseModel"
PydanticV2BaseModel = "pydantic_v2.BaseModel"
PydanticV2Dataclass = "pydantic_v2.dataclass"
DataclassesDataclass = "dataclasses.dataclass"
TypingTypedDict = "typing.TypedDict"
MsgspecStruct = "msgspec.Struct"
Expand Down
14 changes: 13 additions & 1 deletion src/datamodel_code_generator/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def get_data_model_types(
from .types import DataTypeManager # noqa: PLC0415

# Pydantic v2 requires TypeAliasType; other output types use TypeAlias for better compatibility
if data_model_type == DataModelType.PydanticV2BaseModel:
if data_model_type in {DataModelType.PydanticV2BaseModel, DataModelType.PydanticV2Dataclass}:
if target_python_version.has_type_statement:
type_alias_class = type_alias.TypeStatement
scalar_class = scalar.DataTypeScalarTypeStatement
Expand Down Expand Up @@ -94,6 +94,18 @@ def get_data_model_types(
scalar_model=scalar_class,
union_model=union_class,
)
if data_model_type == DataModelType.PydanticV2Dataclass:
from .pydantic_v2 import dataclass as pydantic_v2_dataclass # noqa: PLC0415

return DataModelSet(
data_model=pydantic_v2_dataclass.DataClass,
root_model=type_alias_class,
field_model=pydantic_v2_dataclass.DataModelField,
data_type_manager=pydantic_v2.DataTypeManager,
dump_resolve_reference_action=None,
scalar_model=scalar_class,
union_model=union_class,
)
if data_model_type == DataModelType.DataclassesDataclass:
return DataModelSet(
data_model=dataclass.DataClass,
Expand Down
2 changes: 0 additions & 2 deletions src/datamodel_code_generator/model/pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from .base_model import BaseModel, DataModelField
from .custom_root_type import CustomRootType
from .dataclass import DataClass
from .types import DataTypeManager

if TYPE_CHECKING:
Expand Down Expand Up @@ -41,7 +40,6 @@ class Config(_BaseModel):
__all__ = [
"BaseModel",
"CustomRootType",
"DataClass",
"DataModelField",
"DataTypeManager",
"dump_resolve_reference_action",
Expand Down
21 changes: 0 additions & 21 deletions src/datamodel_code_generator/model/pydantic/dataclass.py

This file was deleted.

1 change: 0 additions & 1 deletion src/datamodel_code_generator/model/pydantic/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,3 @@
IMPORT_STRICT_STR = Import.from_full_path("pydantic.StrictStr")
IMPORT_STRICT_BOOL = Import.from_full_path("pydantic.StrictBool")
IMPORT_STRICT_BYTES = Import.from_full_path("pydantic.StrictBytes")
IMPORT_DATACLASS = Import.from_full_path("pydantic.dataclasses.dataclass")
152 changes: 152 additions & 0 deletions src/datamodel_code_generator/model/pydantic_v2/dataclass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
"""Pydantic v2 dataclass model generator.
Comment thread
koxudaxi marked this conversation as resolved.

Generates pydantic.dataclasses.dataclass decorated classes with validation support.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, ClassVar

from datamodel_code_generator.model import DataModel, DataModelFieldBase
from datamodel_code_generator.model.base import UNDEFINED
from datamodel_code_generator.model.dataclass import has_field_assignment
from datamodel_code_generator.model.pydantic_v2.base_model import Constraints
from datamodel_code_generator.model.pydantic_v2.base_model import (
DataModelField as DataModelFieldV2,
)
from datamodel_code_generator.model.pydantic_v2.imports import (
IMPORT_CONFIG_DICT,
IMPORT_PYDANTIC_DATACLASS,
)
from datamodel_code_generator.reference import Reference

if TYPE_CHECKING:
from collections import defaultdict
from pathlib import Path

from datamodel_code_generator import DataclassArguments
from datamodel_code_generator.imports import Import


class DataClass(DataModel):
"""DataModel implementation for Pydantic v2 dataclasses."""

TEMPLATE_FILE_PATH: ClassVar[str] = "pydantic_v2/dataclass.jinja2"
DEFAULT_IMPORTS: ClassVar[tuple[Import, ...]] = (IMPORT_PYDANTIC_DATACLASS,)
SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True
SUPPORTS_KW_ONLY: ClassVar[bool] = True

def __init__( # noqa: PLR0913
self,
*,
reference: Reference,
fields: list[DataModelFieldBase],
decorators: list[str] | None = None,
base_classes: list[Reference] | None = None,
custom_base_class: str | None = None,
custom_template_dir: Path | None = None,
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
methods: list[str] | None = None,
path: Path | None = None,
description: str | None = None,
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
frozen: bool = False,
treat_dot_as_module: bool | None = None,
dataclass_arguments: DataclassArguments | None = None,
) -> None:
"""Initialize pydantic v2 dataclass with sorted fields and ConfigDict support."""
super().__init__(
reference=reference,
fields=sorted(fields, key=has_field_assignment),
decorators=decorators,
base_classes=base_classes,
custom_base_class=custom_base_class,
custom_template_dir=custom_template_dir,
extra_template_data=extra_template_data,
methods=methods,
path=path,
description=description,
default=default,
nullable=nullable,
keyword_only=keyword_only,
frozen=frozen,
treat_dot_as_module=treat_dot_as_module,
)

if dataclass_arguments is not None:
self.dataclass_arguments = dataclass_arguments
else:
self.dataclass_arguments = {}
if frozen:
self.dataclass_arguments["frozen"] = True
if keyword_only:
self.dataclass_arguments["kw_only"] = True

config_parameters: dict[str, Any] = {}

extra = self._get_config_extra()
if extra:
config_parameters["extra"] = extra

if self.extra_template_data.get("use_attribute_docstrings"):
config_parameters["use_attribute_docstrings"] = True

for data_type in self.all_data_types:
if data_type.is_custom_type: # pragma: no cover
config_parameters["arbitrary_types_allowed"] = True
break

if config_parameters:
self._additional_imports.append(IMPORT_CONFIG_DICT)
self.extra_template_data["config"] = config_parameters

def _get_config_extra(self) -> str | None:
"""Get extra field configuration for ConfigDict."""
additional_properties = self.extra_template_data.get("additionalProperties")
allow_extra_fields = self.extra_template_data.get("allow_extra_fields")
extra_fields = self.extra_template_data.get("extra_fields")

if allow_extra_fields or extra_fields == "allow":
return "'allow'"
if extra_fields == "forbid":
return "'forbid'"
if extra_fields == "ignore":
return "'ignore'"
if additional_properties is True:
return "'allow'"
if additional_properties is False:
return "'forbid'"
return None

def create_reuse_model(self, base_ref: Reference) -> DataClass:
"""Create inherited model with empty fields pointing to base reference."""
return self.__class__(
fields=[],
base_classes=[base_ref],
description=self.description,
reference=Reference(
name=self.name,
path=self.reference.path + "/reuse",
),
custom_template_dir=self._custom_template_dir,
custom_base_class=self.custom_base_class,
keyword_only=self.keyword_only,
frozen=self.frozen,
treat_dot_as_module=self._treat_dot_as_module,
dataclass_arguments=self.dataclass_arguments,
)


class DataModelField(DataModelFieldV2):
"""Field implementation for Pydantic v2 dataclass models.

Inherits pydantic v2 Field() constraint handling from DataModelFieldV2.
"""

constraints: Constraints | None = None # pyright: ignore[reportIncompatibleVariableOverride]

def process_const(self) -> None:
"""Process const field constraint using literal type."""
self._process_const_as_literal()
1 change: 1 addition & 0 deletions src/datamodel_code_generator/model/pydantic_v2/imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@
IMPORT_FUTURE_DATE = Import.from_full_path("pydantic.FutureDate")
IMPORT_BASE64STR = Import.from_full_path("pydantic.Base64Str")
IMPORT_SERIALIZE_AS_ANY = Import.from_full_path("pydantic.SerializeAsAny")
IMPORT_PYDANTIC_DATACLASS = Import.from_full_path("pydantic.dataclasses.dataclass")

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{% for decorator in decorators -%}
{{ decorator }}
{% endfor -%}
{%- set args = [] %}
{%- for k, v in (dataclass_arguments or {}).items() %}
{%- if v is not none and v is not false %}
{%- set _ = args.append(k ~ '=' ~ (v|pprint)) %}
{%- endif %}
{%- endfor %}
{%- if config %}
{%- set config_items = [] %}
{%- for k, v in config.items() %}
{%- set _ = config_items.append(k ~ '=' ~ v) %}
{%- endfor %}
{%- set _ = args.append('config=ConfigDict(' ~ config_items | join(', ') ~ ')') %}
{%- endif %}
{%- if args %}
@dataclass({{ args | join(', ') }})
{%- else %}
@dataclass
{%- endif %}
{%- if base_class %}
class {{ class_name }}({{ base_class }}):
{%- else %}
class {{ class_name }}:
{%- endif %}
{%- if description %}
"""
{{ description | indent(4) }}
"""
{%- endif %}
{%- if not fields and not description %}
pass
{%- endif %}
{%- for field in fields -%}
{%- if not field.annotated and field.field %}
{{ field.name }}: {{ field.type_hint }} = {{ field.field }}
{%- else %}
{%- if field.annotated %}
{{ field.name }}: {{ field.annotated }}
{%- else %}
{{ field.name }}: {{ field.type_hint }}
{%- endif %}
{%- if not field.has_default_factory_in_field and not field.required and (field.represented_default != 'None' or not field.strip_default_none or field.data_type.is_optional)
%} = {{ field.represented_default }}
{%- endif -%}
{%- endif %}
{%- if field.docstring %}
"""
{{ field.docstring | indent(4) }}
"""
{%- if field.use_inline_field_description and not loop.last %}

{% endif %}
{%- elif field.inline_field_docstring %}
{{ field.inline_field_docstring }}
{%- if not loop.last %}

{% endif %}
{%- endif %}
{%- endfor -%}
3 changes: 2 additions & 1 deletion src/datamodel_code_generator/parser/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from datamodel_code_generator.model import pydantic as pydantic_model
from datamodel_code_generator.model.dataclass import DataClass
from datamodel_code_generator.model.enum import SPECIALIZED_ENUM_TYPE_MATCH, Enum
from datamodel_code_generator.model.pydantic_v2.dataclass import DataClass as PydanticV2DataClass
from datamodel_code_generator.model.scalar import DataTypeScalar
from datamodel_code_generator.model.union import DataTypeUnion
from datamodel_code_generator.parser.base import (
Expand Down Expand Up @@ -353,7 +354,7 @@ def _resolve_types(self, paths: list[str], schema: graphql.GraphQLSchema) -> Non
def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
"""Create data model instance with dataclass_arguments support for DataClass."""
data_model_class = model_type or self.data_model_type
if issubclass(data_model_class, DataClass):
if issubclass(data_model_class, (DataClass, PydanticV2DataClass)):
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments
# If both are None, construct from legacy frozen_dataclasses/keyword_only flags
dataclass_arguments = kwargs.pop("dataclass_arguments", None)
Expand Down
3 changes: 2 additions & 1 deletion src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
Enum,
StrEnum,
)
from datamodel_code_generator.model.pydantic_v2.dataclass import DataClass as PydanticV2DataClass
from datamodel_code_generator.parser import DefaultPutDict, LiteralType
from datamodel_code_generator.parser.base import (
SPECIAL_PATH_FORMAT,
Expand Down Expand Up @@ -1739,7 +1740,7 @@ def parse_one_of(self, name: str, obj: JsonSchemaObject, path: list[str]) -> lis
def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
"""Create data model instance with dataclass_arguments support for DataClass."""
data_model_class = model_type or self.data_model_type
if issubclass(data_model_class, DataClass):
if issubclass(data_model_class, (DataClass, PydanticV2DataClass)):
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments
# If both are None, construct from legacy frozen_dataclasses/keyword_only flags
dataclass_arguments = kwargs.pop("dataclass_arguments", None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# generated by datamodel-codegen:
# filename: simple_string.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic.dataclasses import dataclass


@dataclass
class Model:
s: str
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# generated by datamodel-codegen:
# filename: pydantic_v2_dataclass_additional_props_true.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import ConfigDict
from pydantic.dataclasses import dataclass


@dataclass(config=ConfigDict(extra='allow'))
class Model:
s: str
Loading
Loading