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
116 changes: 116 additions & 0 deletions docs/class-decorators.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!-- related-cli-options: --class-decorators, --additional-imports -->

# Custom Class Decorators

The `--class-decorators` option adds custom decorators to all generated model classes. This is useful for integrating with serialization libraries like `dataclasses_json`, or adding custom behavior to your models.

## Why use this?

When using `dataclasses.dataclass` output with `--snake-case-field`, Python field names are snake_case but the original JSON keys may be camelCase. Libraries like `dataclasses_json` can handle this conversion automatically via decorators.

## Example: Using dataclasses_json

Convert a JSON Schema with camelCase properties to dataclasses with snake_case fields that serialize back to camelCase.

**schema.json**
```json
{
"type": "object",
"title": "User",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"emailAddress": { "type": "string" }
},
"required": ["firstName", "lastName"]
}
```

### Without `--class-decorators`

```bash
datamodel-codegen --input schema.json \
--output-model-type dataclasses.dataclass \
--snake-case-field
```

**Generated model.py**
```python
from __future__ import annotations

from dataclasses import dataclass


@dataclass
class User:
first_name: str
last_name: str
email_address: str | None = None
```

The field names are snake_case, but there's no way to map them back to the original camelCase JSON keys.

---

### With `--class-decorators`

```bash
datamodel-codegen --input schema.json \
--output-model-type dataclasses.dataclass \
--snake-case-field \
--class-decorators "@dataclass_json(letter_case=LetterCase.CAMEL)" \
--additional-imports "dataclasses_json.dataclass_json,dataclasses_json.LetterCase"
```

**Generated model.py**
```python
from __future__ import annotations

from dataclasses import dataclass

from dataclasses_json import LetterCase, dataclass_json


@dataclass_json(letter_case=LetterCase.CAMEL)
@dataclass
class User:
first_name: str
last_name: str
email_address: str | None = None
```

Now serialization automatically converts between snake_case and camelCase:

```python
user = User(first_name="John", last_name="Doe")
print(user.to_json())
# {"firstName": "John", "lastName": "Doe", "emailAddress": null}
```

## Usage Notes

- **Multiple decorators**: Use comma separation for multiple decorators:
```bash
--class-decorators "@decorator1,@decorator2"
```

- **@ prefix is optional**: Both `@dataclass_json` and `dataclass_json` work - the `@` is added automatically if missing.

- **Combine with `--additional-imports`**: Always add the required imports for your decorators using `--additional-imports`.

## Other Use Cases

The `--class-decorators` option works with any output model type:

- **Pydantic models**: Add custom validators or behavior
- **TypedDict**: Add runtime type checking decorators
- **msgspec.Struct**: Add custom serialization hooks

## See Also

- [CLI Reference: `--class-decorators`](cli-reference/template-customization.md#class-decorators) - Detailed CLI option documentation
- [CLI Reference: `--additional-imports`](cli-reference/template-customization.md#additional-imports) - Adding custom imports

## Related Issues

- [#2358](https://github.com/koxudaxi/datamodel-code-generator/issues/2358) - Feature request for dataclasses_json support
3 changes: 2 additions & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This documentation is auto-generated from test cases.
| 🔧 [Typing Customization](typing-customization.md) | 22 | Type annotation and import behavior |
| 🏷️ [Field Customization](field-customization.md) | 21 | Field naming and docstring behavior |
| 🏗️ [Model Customization](model-customization.md) | 31 | Model generation behavior |
| 🎨 [Template Customization](template-customization.md) | 16 | Output formatting and custom rendering |
| 🎨 [Template Customization](template-customization.md) | 17 | Output formatting and custom rendering |
| 📘 [OpenAPI-only Options](openapi-only-options.md) | 6 | OpenAPI-specific features |
| ⚙️ [General Options](general-options.md) | 14 | Utilities and meta options |
| 📝 [Utility Options](utility-options.md) | 5 | Help, version, debug options |
Expand Down Expand Up @@ -41,6 +41,7 @@ This documentation is auto-generated from test cases.

- [`--capitalize-enum-members`](field-customization.md#capitalize-enum-members)
- [`--check`](general-options.md#check)
- [`--class-decorators`](template-customization.md#class-decorators)
- [`--class-name`](model-customization.md#class-name)
- [`--collapse-reuse-models`](model-customization.md#collapse-reuse-models)
- [`--collapse-root-models`](model-customization.md#collapse-root-models)
Expand Down
2 changes: 2 additions & 0 deletions docs/cli-reference/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ datamodel-codegen [OPTIONS]
| Option | Description |
|--------|-------------|
| [`--additional-imports`](template-customization.md#additional-imports) | Add custom imports to generated output files. |
| [`--class-decorators`](template-customization.md#class-decorators) | Add custom decorators to generated model classes. |
| [`--custom-file-header`](template-customization.md#custom-file-header) | Add custom header text to the generated file. |
| [`--custom-file-header-path`](template-customization.md#custom-file-header-path) | Add custom header content from file to generated code. |
| [`--custom-formatters`](template-customization.md#custom-formatters) | Apply custom Python code formatters to generated output. |
Expand Down Expand Up @@ -189,6 +190,7 @@ All options sorted alphabetically:
- [`--base-class-map`](model-customization.md#base-class-map) - Test --base-class-map option for model-specific base classes...
- [`--capitalize-enum-members`](field-customization.md#capitalize-enum-members) - Capitalize enum member names to UPPER_CASE format.
- [`--check`](general-options.md#check) - Verify generated code matches existing output without modify...
- [`--class-decorators`](template-customization.md#class-decorators) - Add custom decorators to generated model classes.
- [`--class-name`](model-customization.md#class-name) - Override the auto-generated class name with a custom name.
- [`--collapse-reuse-models`](model-customization.md#collapse-reuse-models) - Collapse duplicate models by replacing references instead of...
- [`--collapse-root-models`](model-customization.md#collapse-root-models) - Inline root model definitions instead of creating separate w...
Expand Down
74 changes: 74 additions & 0 deletions docs/cli-reference/template-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| Option | Description |
|--------|-------------|
| [`--additional-imports`](#additional-imports) | Add custom imports to generated output files. |
| [`--class-decorators`](#class-decorators) | Add custom decorators to generated model classes. |
| [`--custom-file-header`](#custom-file-header) | Add custom header text to the generated file. |
| [`--custom-file-header-path`](#custom-file-header-path) | Add custom header content from file to generated code. |
| [`--custom-formatters`](#custom-formatters) | Apply custom Python code formatters to generated output. |
Expand Down Expand Up @@ -32,6 +33,8 @@ comma-delimited list that will be added to the generated output file. This
is useful when using custom types defined in external modules (e.g.,
"datetime.datetime,datetime.date,mymodule.myclass.MyCustomPythonClass").

**See also:** [Custom Class Decorators](../class-decorators.md)

!!! tip "Usage"

```bash
Expand Down Expand Up @@ -107,6 +110,77 @@ is useful when using custom types defined in external modules (e.g.,

---

## `--class-decorators` {#class-decorators}

Add custom decorators to generated model classes.

The `--class-decorators` option adds custom decorators to all generated model classes.
This is useful for integrating with serialization libraries like `dataclasses_json`.

Use with `--additional-imports` to add the required imports for the decorators.
The `@` prefix is optional and will be added automatically if missing.

**Related:** [`--additional-imports`](template-customization.md#additional-imports), [`--output-model-type`](model-customization.md#output-model-type)

**See also:** [Custom Class Decorators](../class-decorators.md)

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --output-model-type dataclasses.dataclass --class-decorators @dataclass_json --additional-imports dataclasses_json.dataclass_json # (1)!
```

1. :material-arrow-left: `--class-decorators` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"$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"]
}
```

**Output:**

```python
# 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 dataclasses_json import dataclass_json


@dataclass_json
@dataclass
class User:
name: str
age: int
email: str | None = None
```

---

## `--custom-file-header` {#custom-file-header}

Add custom header text to the generated file.
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
base_class: str = "",
base_class_map: dict[str, str] | None = None,
additional_imports: list[str] | None = None,
class_decorators: list[str] | None = None,
custom_template_dir: Path | None = None,
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
validation: bool = False,
Expand Down Expand Up @@ -678,6 +679,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
base_class=base_class,
base_class_map=base_class_map,
additional_imports=additional_imports,
class_decorators=class_decorators,
custom_template_dir=custom_template_dir,
extra_template_data=extra_template_data,
target_python_version=target_python_version,
Expand Down
17 changes: 17 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,21 @@ def validate_custom_formatters(cls, values: dict[str, Any]) -> dict[str, Any]:
values["custom_formatters"] = custom_formatters.split(",")
return values

@model_validator(mode="before")
def validate_class_decorators(cls, values: dict[str, Any]) -> dict[str, Any]: # noqa: N805
Comment thread Dismissed
"""Validate and split class decorators, adding @ prefix if missing."""
class_decorators = values.get("class_decorators")
if class_decorators is not None:
decorators = []
for raw_decorator in class_decorators.split(","):
stripped = raw_decorator.strip()
if stripped:
if not stripped.startswith("@"):
stripped = f"@{stripped}"
decorators.append(stripped)
values["class_decorators"] = decorators
return values

__validate_output_datetime_class_err: ClassVar[str] = (
'`--output-datetime-class` only allows "datetime" for '
f"`--output-model-type` {DataModelType.DataclassesDataclass.value}"
Expand Down Expand Up @@ -383,6 +398,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
base_class: str = ""
base_class_map: Optional[dict[str, str]] = None # noqa: UP045
additional_imports: Optional[list[str]] = None # noqa: UP045
class_decorators: Optional[list[str]] = None # noqa: UP045
custom_template_dir: Optional[Path] = None # noqa: UP045
extra_template_data: Optional[TextIOBase] = None # noqa: UP045
validation: bool = False
Expand Down Expand Up @@ -697,6 +713,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
base_class=config.base_class,
base_class_map=config.base_class_map,
additional_imports=config.additional_imports,
class_decorators=config.class_decorators,
custom_template_dir=config.custom_template_dir,
validation=config.validation,
field_constraints=config.field_constraints,
Expand Down
8 changes: 8 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,14 @@ def start_section(self, heading: str | None) -> None:
type=str,
default=None,
)
base_options.add_argument(
"--class-decorators",
help="Custom decorators for generated model classes (delimited list input). "
'For example "@dataclass_json(letter_case=LetterCase.CAMEL)". '
'The "@" prefix is optional and will be added automatically if missing.',
type=str,
default=None,
)
base_options.add_argument(
"--formatters",
help="Formatters for output (default: [black, isort])",
Expand Down
1 change: 1 addition & 0 deletions src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ class CLIOptionMeta:
"--custom-file-header": CLIOptionMeta(name="--custom-file-header", category=OptionCategory.TEMPLATE),
"--custom-file-header-path": CLIOptionMeta(name="--custom-file-header-path", category=OptionCategory.TEMPLATE),
"--additional-imports": CLIOptionMeta(name="--additional-imports", category=OptionCategory.TEMPLATE),
"--class-decorators": CLIOptionMeta(name="--class-decorators", category=OptionCategory.TEMPLATE),
"--use-double-quotes": CLIOptionMeta(name="--use-double-quotes", category=OptionCategory.TEMPLATE),
"--use-exact-imports": CLIOptionMeta(name="--use-exact-imports", category=OptionCategory.TEMPLATE),
"--disable-appending-item-suffix": CLIOptionMeta(
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
base_class: str | None = None,
base_class_map: dict[str, str] | None = None,
additional_imports: list[str] | None = None,
class_decorators: list[str] | None = None,
custom_template_dir: Path | None = None,
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
target_python_version: PythonVersion = PythonVersionMin,
Expand Down Expand Up @@ -807,6 +808,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
self.imports: Imports = Imports(use_exact_imports)
self.use_exact_imports: bool = use_exact_imports
self._append_additional_imports(additional_imports=additional_imports)
self.class_decorators: list[str] = class_decorators or []

self.base_class: str | None = base_class
self.base_class_map: dict[str, str] | None = base_class_map
Expand Down
5 changes: 5 additions & 0 deletions src/datamodel_code_generator/parser/graphql.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def __init__( # noqa: PLR0913
base_class: str | None = None,
base_class_map: dict[str, str] | None = None,
additional_imports: list[str] | None = None,
class_decorators: list[str] | None = None,
custom_template_dir: Path | None = None,
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
target_python_version: PythonVersion = PythonVersionMin,
Expand Down Expand Up @@ -214,6 +215,7 @@ def __init__( # noqa: PLR0913
base_class=base_class,
base_class_map=base_class_map,
additional_imports=additional_imports,
class_decorators=class_decorators,
custom_template_dir=custom_template_dir,
extra_template_data=extra_template_data,
target_python_version=target_python_version,
Expand Down Expand Up @@ -362,6 +364,9 @@ 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."""
# Add class decorators if not already provided
if "decorators" not in kwargs and self.class_decorators:
kwargs["decorators"] = list(self.class_decorators)
data_model_class = model_type or self.data_model_type
if issubclass(data_model_class, (DataClass, PydanticV2DataClass)):
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments
Expand Down
5 changes: 5 additions & 0 deletions src/datamodel_code_generator/parser/jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ def __init__( # noqa: PLR0913
base_class: str | None = None,
base_class_map: dict[str, str] | None = None,
additional_imports: list[str] | None = None,
class_decorators: list[str] | None = None,
custom_template_dir: Path | None = None,
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
target_python_version: PythonVersion = PythonVersionMin,
Expand Down Expand Up @@ -638,6 +639,7 @@ def __init__( # noqa: PLR0913
base_class=base_class,
base_class_map=base_class_map,
additional_imports=additional_imports,
class_decorators=class_decorators,
custom_template_dir=custom_template_dir,
extra_template_data=extra_template_data,
target_python_version=target_python_version,
Expand Down Expand Up @@ -1760,6 +1762,9 @@ 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."""
# Add class decorators if not already provided
if "decorators" not in kwargs and self.class_decorators:
kwargs["decorators"] = list(self.class_decorators)
data_model_class = model_type or self.data_model_type
if issubclass(data_model_class, (DataClass, PydanticV2DataClass)):
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments
Expand Down
Loading
Loading