Repository: koxudaxi/datamodel-code-generator

Issue:
Support for frozen dataclasses
**Is your feature request related to a problem? Please describe.**
Not really a problem per say. But many of our current models are frozen dataclasses. We try to keep them immutable by default. But unfortunately theres not a flag for it when generating through this tool. 

We will probably still explore generating these just as not frozen to start, so not really a blocker. But it would be awesome if we could bring that in soon.

**Describe the solution you'd like**
It seems like it would be straightforward to utilize the already available `--enable-faux-immutability` flag.

**Describe alternatives you've considered**
Could add another flag of `--frozen-dataclasses` or similar. 

**Additional context**
Nothin that I can think of. If I have time I will try to get a PR up if desired :)


Discussion:
My team could also use the addition of such feature (more in the context of Pydantic dataclasses, but I suspect that those would be related).
Hey @Sheemap and @Iddodo ,

I am curious what you are trying to do.  

When I generate PydanticV2 models, with the `--enable-faux-immutability` flag, I get frozen dataclasses: 

```python
class MyTHing(BaseModel):
    model_config = ConfigDict(
        extra="allow",
        frozen=True,
    )
    ...
```



Hi @JakeSummers, In this case specifically we're talking about python's `dataclasses` module. Which you can generate by passing `--output-model-type dataclasses.dataclass`. Which gives you this

```python3
@dataclass
class MyTHing:
    ...
```

Theres a `frozen` param you can pass to the decorator to lock all the fields, making it immutable.  But datamodel-code-generator doesn't support this currently.

```python3
@dataclass(frozen=True)
class MyTHing:
    ...
```

This second one is what I'm specifically trying to accomplish
@Sheemap 
Thank you for bringing this up! I completely agree that having frozen dataclasses would be really useful. We have many users who prefer immutable data structures, so this would be a great addition. When you have time, feel free to submit a PR - either leveraging the existing flag or adding a new one. I'd be happy to review and help get it merged.



Patch:
diff --git a/README.md b/README.md
index 34d2d52fb..1081bbd3a 100644
--- a/README.md
+++ b/README.md
@@ -455,6 +455,8 @@ Model customization:
                         Enable faux immutability
   --enable-version-header
                         Enable package version on file headers
+  --frozen-dataclasses  Generate frozen dataclasses (dataclass(frozen=True)). Only applies
+                        to dataclass output.
   --keep-model-order    Keep generated models'' order
   --keyword-only        Defined models as keyword only (for example
                         dataclass(kw_only=True)).
diff --git a/docs/index.md b/docs/index.md
index 3f8c79713..9dfb8ef36 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -447,6 +447,8 @@ Model customization:
                         Enable faux immutability
   --enable-version-header
                         Enable package version on file headers
+  --frozen-dataclasses  Generate frozen dataclasses (dataclass(frozen=True)). Only applies
+                        to dataclass output.
   --keep-model-order    Keep generated models'' order
   --keyword-only        Defined models as keyword only (for example
                         dataclass(kw_only=True)).
diff --git a/src/datamodel_code_generator/__init__.py b/src/datamodel_code_generator/__init__.py
index 86472b08f..722598db9 100644
--- a/src/datamodel_code_generator/__init__.py
+++ b/src/datamodel_code_generator/__init__.py
@@ -285,6 +285,7 @@ def generate(  # noqa: PLR0912, PLR0913, PLR0914, PLR0915
     union_mode: UnionMode | None = None,
     output_datetime_class: DatetimeClassType | None = None,
     keyword_only: bool = False,
+    frozen_dataclasses: bool = False,
     no_alias: bool = False,
     formatters: list[Formatter] = DEFAULT_FORMATTERS,
     parent_scoped_naming: bool = False,
@@ -481,6 +482,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
         default_field_extras=default_field_extras,
         target_datetime_class=output_datetime_class,
         keyword_only=keyword_only,
+        frozen_dataclasses=frozen_dataclasses,
         no_alias=no_alias,
         formatters=formatters,
         encoding=encoding,
diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py
index e04b3e824..091bc62a0 100644
--- a/src/datamodel_code_generator/__main__.py
+++ b/src/datamodel_code_generator/__main__.py
@@ -312,6 +312,7 @@ def validate_root(cls, values: Any) -> Any:  # noqa: N805
     union_mode: Optional[UnionMode] = None  # noqa: UP045
     output_datetime_class: Optional[DatetimeClassType] = None  # noqa: UP045
     keyword_only: bool = False
+    frozen_dataclasses: bool = False
     no_alias: bool = False
     formatters: list[Formatter] = DEFAULT_FORMATTERS
     parent_scoped_naming: bool = False
@@ -524,6 +525,7 @@ def main(args: Sequence[str] | None = None) -> Exit:  # noqa: PLR0911, PLR0912,
             union_mode=config.union_mode,
             output_datetime_class=config.output_datetime_class,
             keyword_only=config.keyword_only,
+            frozen_dataclasses=config.frozen_dataclasses,
             no_alias=config.no_alias,
             formatters=config.formatters,
             parent_scoped_naming=config.parent_scoped_naming,
diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py
index 599cbb7f2..8875d23b3 100644
--- a/src/datamodel_code_generator/arguments.py
+++ b/src/datamodel_code_generator/arguments.py
@@ -153,6 +153,12 @@ def start_section(self, heading: str | None) -> None:
     action="store_true",
     default=None,
 )
+model_options.add_argument(
+    "--frozen-dataclasses",
+    help="Generate frozen dataclasses (dataclass(frozen=True)). Only applies to dataclass output.",
+    action="store_true",
+    default=None,
+)
 model_options.add_argument(
     "--reuse-model",
     help="Reuse models on the field when a module has the model with the same content",
diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py
index e2cefcaed..fdaec5136 100644
--- a/src/datamodel_code_generator/model/base.py
+++ b/src/datamodel_code_generator/model/base.py
@@ -275,9 +275,11 @@ def __init__(  # noqa: PLR0913
         default: Any = UNDEFINED,
         nullable: bool = False,
         keyword_only: bool = False,
+        frozen: bool = False,
         treat_dot_as_module: bool = False,
     ) -> None:
         self.keyword_only = keyword_only
+        self.frozen = frozen
         if not self.TEMPLATE_FILE_PATH:
             msg = "TEMPLATE_FILE_PATH is undefined"
             raise Exception(msg)  # noqa: TRY002
@@ -434,5 +436,6 @@ def render(self, *, class_name: str | None = None) -> str:
             methods=self.methods,
             description=self.description,
             keyword_only=self.keyword_only,
+            frozen=self.frozen,
             **self.extra_template_data,
         )
diff --git a/src/datamodel_code_generator/model/dataclass.py b/src/datamodel_code_generator/model/dataclass.py
index 190c6f631..71c2d1822 100644
--- a/src/datamodel_code_generator/model/dataclass.py
+++ b/src/datamodel_code_generator/model/dataclass.py
@@ -53,6 +53,7 @@ def __init__(  # noqa: PLR0913
         default: Any = UNDEFINED,
         nullable: bool = False,
         keyword_only: bool = False,
+        frozen: bool = False,
         treat_dot_as_module: bool = False,
     ) -> None:
         super().__init__(
@@ -69,6 +70,7 @@ def __init__(  # noqa: PLR0913
             default=default,
             nullable=nullable,
             keyword_only=keyword_only,
+            frozen=frozen,
             treat_dot_as_module=treat_dot_as_module,
         )
 
diff --git a/src/datamodel_code_generator/model/template/dataclass.jinja2 b/src/datamodel_code_generator/model/template/dataclass.jinja2
index 632026b59..3c25ae72b 100644
--- a/src/datamodel_code_generator/model/template/dataclass.jinja2
+++ b/src/datamodel_code_generator/model/template/dataclass.jinja2
@@ -1,7 +1,14 @@
 {% for decorator in decorators -%}
 {{ decorator }}
 {% endfor -%}
-@dataclass{%- if keyword_only -%}(kw_only=True){%- endif %}
+@dataclass
+{%- if keyword_only or frozen -%}
+(
+{%- if keyword_only -%}kw_only=True{%- endif -%}
+{%- if keyword_only and frozen -%}, {% endif -%}
+{%- if frozen -%}frozen=True{%- endif -%}
+)
+{%- endif %}
 {%- if base_class %}
 class {{ class_name }}({{ base_class }}):
 {%- else %}
diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py
index 098ceea49..a6642ba27 100644
--- a/src/datamodel_code_generator/parser/base.py
+++ b/src/datamodel_code_generator/parser/base.py
@@ -377,11 +377,13 @@ def __init__(  # noqa: PLR0913, PLR0915
         default_field_extras: dict[str, Any] | None = None,
         target_datetime_class: DatetimeClassType | None = DatetimeClassType.Datetime,
         keyword_only: bool = False,
+        frozen_dataclasses: bool = False,
         no_alias: bool = False,
         formatters: list[Formatter] = DEFAULT_FORMATTERS,
         parent_scoped_naming: bool = False,
     ) -> None:
         self.keyword_only = keyword_only
+        self.frozen_dataclasses = frozen_dataclasses
         self.data_type_manager: DataTypeManager = data_type_manager_type(
             python_version=target_python_version,
             use_standard_collections=use_standard_collections,
diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py
index 107a51646..e953ac092 100644
--- a/src/datamodel_code_generator/parser/graphql.py
+++ b/src/datamodel_code_generator/parser/graphql.py
@@ -17,6 +17,7 @@
 )
 from datamodel_code_generator.model import DataModel, DataModelFieldBase
 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 Enum
 from datamodel_code_generator.model.scalar import DataTypeScalar
 from datamodel_code_generator.model.union import DataTypeUnion
@@ -152,6 +153,7 @@ def __init__(  # noqa: PLR0913
         default_field_extras: dict[str, Any] | None = None,
         target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime,
         keyword_only: bool = False,
+        frozen_dataclasses: bool = False,
         no_alias: bool = False,
         formatters: list[Formatter] = DEFAULT_FORMATTERS,
         parent_scoped_naming: bool = False,
@@ -227,6 +229,7 @@ def __init__(  # noqa: PLR0913
             default_field_extras=default_field_extras,
             target_datetime_class=target_datetime_class,
             keyword_only=keyword_only,
+            frozen_dataclasses=frozen_dataclasses,
             no_alias=no_alias,
             formatters=formatters,
             parent_scoped_naming=parent_scoped_naming,
@@ -283,6 +286,13 @@ def _resolve_types(self, paths: list[str], schema: graphql.GraphQLSchema) -> Non
 
                 self.support_graphql_types[resolved_type].append(type_)
 
+    def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
+        """Create data model instance with conditional frozen parameter for DataClass."""
+        data_model_class = model_type or self.data_model_type
+        if issubclass(data_model_class, DataClass):
+            kwargs["frozen"] = self.frozen_dataclasses
+        return data_model_class(**kwargs)
+
     def _typename_field(self, name: str) -> DataModelFieldBase:
         return self.data_model_field_type(
             name="typename__",
@@ -445,7 +455,7 @@ def parse_object_like(
         if hasattr(obj, "interfaces"):  # pragma: no cover
             base_classes = [self.references[i.name] for i in obj.interfaces]  # pyright: ignore[reportAttributeAccessIssue]
 
-        data_model_type = self.data_model_type(
+        data_model_type = self._create_data_model(
             reference=self.references[obj.name],
             fields=fields,
             base_classes=base_classes,
diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py
index 09cfc0afa..968180853 100644
--- a/src/datamodel_code_generator/parser/jsonschema.py
+++ b/src/datamodel_code_generator/parser/jsonschema.py
@@ -23,6 +23,7 @@
 from datamodel_code_generator.model import DataModel, DataModelFieldBase
 from datamodel_code_generator.model import pydantic as pydantic_model
 from datamodel_code_generator.model.base import UNDEFINED, get_module_name
+from datamodel_code_generator.model.dataclass import DataClass
 from datamodel_code_generator.model.enum import Enum
 from datamodel_code_generator.parser import DefaultPutDict, LiteralType
 from datamodel_code_generator.parser.base import (
@@ -431,6 +432,7 @@ def __init__(  # noqa: PLR0913
         default_field_extras: dict[str, Any] | None = None,
         target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime,
         keyword_only: bool = False,
+        frozen_dataclasses: bool = False,
         no_alias: bool = False,
         formatters: list[Formatter] = DEFAULT_FORMATTERS,
         parent_scoped_naming: bool = False,
@@ -506,6 +508,7 @@ def __init__(  # noqa: PLR0913
             default_field_extras=default_field_extras,
             target_datetime_class=target_datetime_class,
             keyword_only=keyword_only,
+            frozen_dataclasses=frozen_dataclasses,
             no_alias=no_alias,
             formatters=formatters,
             parent_scoped_naming=parent_scoped_naming,
@@ -696,6 +699,13 @@ def parse_any_of(self, name: str, obj: JsonSchemaObject, path: list[str]) -> lis
     def parse_one_of(self, name: str, obj: JsonSchemaObject, path: list[str]) -> list[DataType]:
         return self.parse_combined_schema(name, obj, path, "oneOf")
 
+    def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
+        """Create data model instance with conditional frozen parameter for DataClass."""
+        data_model_class = model_type or self.data_model_type
+        if issubclass(data_model_class, DataClass):
+            kwargs["frozen"] = self.frozen_dataclasses
+        return data_model_class(**kwargs)
+
     def _parse_object_common_part(  # noqa: PLR0913, PLR0917
         self,
         name: str,
@@ -743,7 +753,8 @@ def _parse_object_common_part(  # noqa: PLR0913, PLR0917
             name = obj.title
         reference = self.model_resolver.add(path, name, class_name=True, loaded=True)
         self.set_additional_properties(reference.name, obj)
-        data_model_type = self.data_model_type(
+
+        data_model_type = self._create_data_model(
             reference=reference,
             fields=fields,
             base_classes=base_classes,
@@ -982,7 +993,9 @@ def parse_object(
             data_model_type_class = self.data_model_root_type
 
         self.set_additional_properties(class_name, obj)
-        data_model_type = data_model_type_class(
+
+        data_model_type = self._create_data_model(
+            model_type=data_model_type_class,
             reference=reference,
             fields=fields,
             custom_base_class=obj.custom_base_path or self.base_class,
diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py
index ec8bec14d..3b5942887 100644
--- a/src/datamodel_code_generator/parser/openapi.py
+++ b/src/datamodel_code_generator/parser/openapi.py
@@ -42,6 +42,7 @@
     from pathlib import Path
     from urllib.parse import ParseResult
 
+
 RE_APPLICATION_JSON_PATTERN: Pattern[str] = re.compile(r"^application/.*json$")
 
 OPERATION_NAMES: list[str] = [
@@ -214,6 +215,7 @@ def __init__(  # noqa: PLR0913
         default_field_extras: dict[str, Any] | None = None,
         target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime,
         keyword_only: bool = False,
+        frozen_dataclasses: bool = False,
         no_alias: bool = False,
         formatters: list[Formatter] = DEFAULT_FORMATTERS,
         parent_scoped_naming: bool = False,
@@ -289,6 +291,7 @@ def __init__(  # noqa: PLR0913
             default_field_extras=default_field_extras,
             target_datetime_class=target_datetime_class,
             keyword_only=keyword_only,
+            frozen_dataclasses=frozen_dataclasses,
             no_alias=no_alias,
             formatters=formatters,
             parent_scoped_naming=parent_scoped_naming,
@@ -483,8 +486,10 @@ def parse_all_parameters(
                 )
 
         if OpenAPIScope.Parameters in self.open_api_scopes and fields:
+            # Using _create_data_model from parent class JsonSchemaParser
+            # This method automatically adds frozen=True for DataClass types
             self.results.append(
-                self.data_model_type(
+                self._create_data_model(
                     fields=fields,
                     reference=reference,
                     custom_base_class=self.base_class,


Test patch:
diff --git a/tests/data/expected/main/frozen_dataclasses.py b/tests/data/expected/main/frozen_dataclasses.py
new file mode 100644
index 000000000..6946ddc76
--- /dev/null
+++ b/tests/data/expected/main/frozen_dataclasses.py
@@ -0,0 +1,15 @@
+# generated by datamodel-codegen:
+#   filename:  simple_frozen_test.json
+#   timestamp: 1985-10-26T08:21:00+00:00
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass(frozen=True)
+class User:
+    name: str
+    age: int
+    email: Optional[str] = None
diff --git a/tests/data/expected/main/frozen_dataclasses_keyword_only.py b/tests/data/expected/main/frozen_dataclasses_keyword_only.py
new file mode 100644
index 000000000..6dfc4ee6e
--- /dev/null
+++ b/tests/data/expected/main/frozen_dataclasses_keyword_only.py
@@ -0,0 +1,15 @@
+# generated by datamodel-codegen:
+#   filename:  simple_frozen_test.json
+#   timestamp: 1985-10-26T08:21:00+00:00
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Optional
+
+
+@dataclass(kw_only=True, frozen=True)
+class User:
+    name: str
+    age: int
+    email: Optional[str] = None
diff --git a/tests/data/jsonschema/simple_frozen_test.json b/tests/data/jsonschema/simple_frozen_test.json
new file mode 100644
index 000000000..0e1b94422
--- /dev/null
+++ b/tests/data/jsonschema/simple_frozen_test.json
@@ -0,0 +1,18 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "type": "object",
+  "title": "User",
+  "properties": {
+    "name": {
+      "type": "string"
+    },
+    "age": {
+      "type": "integer"
+    },
+    "email": {
+      "type": "string",
+      "format": "email"
+    }
+  },
+  "required": ["name", "age"]
+}
\ No newline at end of file
diff --git a/tests/main/test_main_general.py b/tests/main/test_main_general.py
index d520689c3..4aedf31a5 100644
--- a/tests/main/test_main_general.py
+++ b/tests/main/test_main_general.py
@@ -15,6 +15,7 @@
     snooper_to_methods,
 )
 from datamodel_code_generator.__main__ import Config, Exit, main
+from datamodel_code_generator.format import PythonVersion
 
 if TYPE_CHECKING:
     from pytest_mock import MockerFixture
@@ -112,3 +113,79 @@ def test_direct_input_dict() -> None:
             snake_case_field=True,
         )
         assert output_file.read_text() == (EXPECTED_MAIN_PATH / "direct_input_dict.py").read_text()
+
+
+@freeze_time(TIMESTAMP)
+def test_frozen_dataclasses() -> None:
+    """Test --frozen-dataclasses flag functionality."""
+    with TemporaryDirectory() as output_dir:
+        output_file = Path(output_dir) / "output.py"
+        generate(
+            DATA_PATH / "jsonschema" / "simple_frozen_test.json",
+            input_file_type=InputFileType.JsonSchema,
+            output=output_file,
+            output_model_type=DataModelType.DataclassesDataclass,
+            frozen_dataclasses=True,
+        )
+        assert output_file.read_text() == (EXPECTED_MAIN_PATH / "frozen_dataclasses.py").read_text()
+
+
+@freeze_time(TIMESTAMP)
+def test_frozen_dataclasses_with_keyword_only() -> None:
+    """Test --frozen-dataclasses with --keyword-only flag combination."""
+
+    with TemporaryDirectory() as output_dir:
+        output_file = Path(output_dir) / "output.py"
+        generate(
+            DATA_PATH / "jsonschema" / "simple_frozen_test.json",
+            input_file_type=InputFileType.JsonSchema,
+            output=output_file,
+            output_model_type=DataModelType.DataclassesDataclass,
+            frozen_dataclasses=True,
+            keyword_only=True,
+            target_python_version=PythonVersion.PY_310,
+        )
+        assert output_file.read_text() == (EXPECTED_MAIN_PATH / "frozen_dataclasses_keyword_only.py").read_text()
+
+
+@freeze_time(TIMESTAMP)
+def test_frozen_dataclasses_command_line() -> None:
+    """Test --frozen-dataclasses flag via command line."""
+    with TemporaryDirectory() as output_dir:
+        output_file: Path = Path(output_dir) / "output.py"
+        return_code: Exit = main([
+            "--input",
+            str(DATA_PATH / "jsonschema" / "simple_frozen_test.json"),
+            "--output",
+            str(output_file),
+            "--input-file-type",
+            "jsonschema",
+            "--output-model-type",
+            "dataclasses.dataclass",
+            "--frozen-dataclasses",
+        ])
+        assert return_code == Exit.OK
+        assert output_file.read_text() == (EXPECTED_MAIN_PATH / "frozen_dataclasses.py").read_text()
+
+
+@freeze_time(TIMESTAMP)
+def test_frozen_dataclasses_with_keyword_only_command_line() -> None:
+    """Test --frozen-dataclasses with --keyword-only flag via command line."""
+    with TemporaryDirectory() as output_dir:
+        output_file: Path = Path(output_dir) / "output.py"
+        return_code: Exit = main([
+            "--input",
+            str(DATA_PATH / "jsonschema" / "simple_frozen_test.json"),
+            "--output",
+            str(output_file),
+            "--input-file-type",
+            "jsonschema",
+            "--output-model-type",
+            "dataclasses.dataclass",
+            "--frozen-dataclasses",
+            "--keyword-only",
+            "--target-python-version",
+            "3.10",
+        ])
+        assert return_code == Exit.OK
+        assert output_file.read_text() == (EXPECTED_MAIN_PATH / "frozen_dataclasses_keyword_only.py").read_text()
diff --git a/tests/model/dataclass/__init__.py b/tests/model/dataclass/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/model/dataclass/test_param.py b/tests/model/dataclass/test_param.py
new file mode 100644
index 000000000..7b9dddf98
--- /dev/null
+++ b/tests/model/dataclass/test_param.py
@@ -0,0 +1,147 @@
+from __future__ import annotations
+
+from datamodel_code_generator.model.dataclass import DataClass, DataModelField
+from datamodel_code_generator.reference import Reference
+from datamodel_code_generator.types import DataType, Types
+
+
+def test_dataclass_without_frozen() -> None:
+    """Test dataclass generation without frozen parameter."""
+    reference = Reference(path="TestModel", name="TestModel")
+    field = DataModelField(
+        name="field1",
+        data_type=DataType(type=Types.string),
+        required=True,
+    )
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[field],
+        frozen=False,
+    )
+
+    rendered = dataclass.render()
+    assert "@dataclass" in rendered
+    assert "frozen=True" not in rendered
+    assert "class TestModel:" in rendered
+
+
+def test_dataclass_with_frozen() -> None:
+    """Test dataclass generation with frozen=True."""
+    reference = Reference(path="TestModel", name="TestModel")
+    field = DataModelField(
+        name="field1",
+        data_type=DataType(type=Types.string),
+        required=True,
+    )
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[field],
+        frozen=True,
+    )
+
+    rendered = dataclass.render()
+    assert "@dataclass(frozen=True)" in rendered
+    assert "class TestModel:" in rendered
+
+
+def test_dataclass_with_keyword_only_and_frozen() -> None:
+    """Test dataclass generation with both keyword_only and frozen parameters."""
+    reference = Reference(path="TestModel", name="TestModel")
+    field = DataModelField(
+        name="field1",
+        data_type=DataType(type=Types.string),
+        required=True,
+    )
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[field],
+        keyword_only=True,
+        frozen=True,
+    )
+
+    rendered = dataclass.render()
+    assert "@dataclass" in rendered
+    assert "kw_only=True" in rendered
+    assert "frozen=True" in rendered
+    assert "class TestModel:" in rendered
+
+
+def test_dataclass_with_only_keyword_only() -> None:
+    """Test dataclass generation with only keyword_only parameter."""
+    reference = Reference(path="TestModel", name="TestModel")
+    field = DataModelField(
+        name="field1",
+        data_type=DataType(type=Types.string),
+        required=True,
+    )
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[field],
+        keyword_only=True,
+        frozen=False,
+    )
+
+    rendered = dataclass.render()
+    assert "@dataclass" in rendered
+    assert "kw_only=True" in rendered
+    assert "frozen=True" not in rendered
+    assert "class TestModel:" in rendered
+
+
+def test_dataclass_frozen_attribute() -> None:
+    """Test that frozen attribute is properly stored."""
+    reference = Reference(path="TestModel", name="TestModel")
+    dataclass = DataClass(
+        reference=reference,
+        fields=[],
+        frozen=True,
+    )
+
+    assert dataclass.frozen is True
+
+
+def test_dataclass_frozen_false_attribute() -> None:
+    """Test that frozen attribute defaults to False."""
+    reference = Reference(path="TestModel", name="TestModel")
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[],
+    )
+
+    assert dataclass.frozen is False
+
+
+def test_dataclass_kw_only_true_only() -> None:
+    """Test dataclass generation with kw_only=True only (comprehensive test)."""
+    reference = Reference(path="TestModel", name="TestModel")
+    field1 = DataModelField(
+        name="field1",
+        data_type=DataType(type=Types.string),
+        required=True,
+    )
+    field2 = DataModelField(
+        name="field2",
+        data_type=DataType(type=Types.integer),
+        required=False,
+    )
+
+    dataclass = DataClass(
+        reference=reference,
+        fields=[field1, field2],
+        keyword_only=True,
+    )
+
+    rendered = dataclass.render()
+    # Should have @dataclass(kw_only=True) but not frozen=True
+    assert "@dataclass(kw_only=True)" in rendered
+    assert "frozen=True" not in rendered
+    assert "class TestModel:" in rendered
+
+    # Verify frozen attribute is False (default)
+    assert dataclass.frozen is False
+    assert dataclass.keyword_only is True