Skip to content

Commit 75d0c06

Browse files
Add --class-decorators option for custom model decorators (#2760)
* Add --class-decorators option for custom model decorators * docs: update CLI reference documentation 🤖 Generated by GitHub Actions * Add test for empty entries in class-decorators * Add parameterized e2e test for class-decorators across all output types --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 7b17da0 commit 75d0c06

21 files changed

Lines changed: 500 additions & 1 deletion

docs/class-decorators.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<!-- related-cli-options: --class-decorators, --additional-imports -->
2+
3+
# Custom Class Decorators
4+
5+
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.
6+
7+
## Why use this?
8+
9+
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.
10+
11+
## Example: Using dataclasses_json
12+
13+
Convert a JSON Schema with camelCase properties to dataclasses with snake_case fields that serialize back to camelCase.
14+
15+
**schema.json**
16+
```json
17+
{
18+
"type": "object",
19+
"title": "User",
20+
"properties": {
21+
"firstName": { "type": "string" },
22+
"lastName": { "type": "string" },
23+
"emailAddress": { "type": "string" }
24+
},
25+
"required": ["firstName", "lastName"]
26+
}
27+
```
28+
29+
### Without `--class-decorators`
30+
31+
```bash
32+
datamodel-codegen --input schema.json \
33+
--output-model-type dataclasses.dataclass \
34+
--snake-case-field
35+
```
36+
37+
**Generated model.py**
38+
```python
39+
from __future__ import annotations
40+
41+
from dataclasses import dataclass
42+
43+
44+
@dataclass
45+
class User:
46+
first_name: str
47+
last_name: str
48+
email_address: str | None = None
49+
```
50+
51+
The field names are snake_case, but there's no way to map them back to the original camelCase JSON keys.
52+
53+
---
54+
55+
### With `--class-decorators`
56+
57+
```bash
58+
datamodel-codegen --input schema.json \
59+
--output-model-type dataclasses.dataclass \
60+
--snake-case-field \
61+
--class-decorators "@dataclass_json(letter_case=LetterCase.CAMEL)" \
62+
--additional-imports "dataclasses_json.dataclass_json,dataclasses_json.LetterCase"
63+
```
64+
65+
**Generated model.py**
66+
```python
67+
from __future__ import annotations
68+
69+
from dataclasses import dataclass
70+
71+
from dataclasses_json import LetterCase, dataclass_json
72+
73+
74+
@dataclass_json(letter_case=LetterCase.CAMEL)
75+
@dataclass
76+
class User:
77+
first_name: str
78+
last_name: str
79+
email_address: str | None = None
80+
```
81+
82+
Now serialization automatically converts between snake_case and camelCase:
83+
84+
```python
85+
user = User(first_name="John", last_name="Doe")
86+
print(user.to_json())
87+
# {"firstName": "John", "lastName": "Doe", "emailAddress": null}
88+
```
89+
90+
## Usage Notes
91+
92+
- **Multiple decorators**: Use comma separation for multiple decorators:
93+
```bash
94+
--class-decorators "@decorator1,@decorator2"
95+
```
96+
97+
- **@ prefix is optional**: Both `@dataclass_json` and `dataclass_json` work - the `@` is added automatically if missing.
98+
99+
- **Combine with `--additional-imports`**: Always add the required imports for your decorators using `--additional-imports`.
100+
101+
## Other Use Cases
102+
103+
The `--class-decorators` option works with any output model type:
104+
105+
- **Pydantic models**: Add custom validators or behavior
106+
- **TypedDict**: Add runtime type checking decorators
107+
- **msgspec.Struct**: Add custom serialization hooks
108+
109+
## See Also
110+
111+
- [CLI Reference: `--class-decorators`](cli-reference/template-customization.md#class-decorators) - Detailed CLI option documentation
112+
- [CLI Reference: `--additional-imports`](cli-reference/template-customization.md#additional-imports) - Adding custom imports
113+
114+
## Related Issues
115+
116+
- [#2358](https://github.com/koxudaxi/datamodel-code-generator/issues/2358) - Feature request for dataclasses_json support

docs/cli-reference/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This documentation is auto-generated from test cases.
1212
| 🔧 [Typing Customization](typing-customization.md) | 22 | Type annotation and import behavior |
1313
| 🏷️ [Field Customization](field-customization.md) | 21 | Field naming and docstring behavior |
1414
| 🏗️ [Model Customization](model-customization.md) | 31 | Model generation behavior |
15-
| 🎨 [Template Customization](template-customization.md) | 16 | Output formatting and custom rendering |
15+
| 🎨 [Template Customization](template-customization.md) | 17 | Output formatting and custom rendering |
1616
| 📘 [OpenAPI-only Options](openapi-only-options.md) | 6 | OpenAPI-specific features |
1717
| ⚙️ [General Options](general-options.md) | 15 | Utilities and meta options |
1818
| 📝 [Utility Options](utility-options.md) | 5 | Help, version, debug options |
@@ -41,6 +41,7 @@ This documentation is auto-generated from test cases.
4141

4242
- [`--capitalize-enum-members`](field-customization.md#capitalize-enum-members)
4343
- [`--check`](general-options.md#check)
44+
- [`--class-decorators`](template-customization.md#class-decorators)
4445
- [`--class-name`](model-customization.md#class-name)
4546
- [`--collapse-reuse-models`](model-customization.md#collapse-reuse-models)
4647
- [`--collapse-root-models`](model-customization.md#collapse-root-models)

docs/cli-reference/quick-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ datamodel-codegen [OPTIONS]
116116
| Option | Description |
117117
|--------|-------------|
118118
| [`--additional-imports`](template-customization.md#additional-imports) | Add custom imports to generated output files. |
119+
| [`--class-decorators`](template-customization.md#class-decorators) | Add custom decorators to generated model classes. |
119120
| [`--custom-file-header`](template-customization.md#custom-file-header) | Add custom header text to the generated file. |
120121
| [`--custom-file-header-path`](template-customization.md#custom-file-header-path) | Add custom header content from file to generated code. |
121122
| [`--custom-formatters`](template-customization.md#custom-formatters) | Apply custom Python code formatters to generated output. |
@@ -190,6 +191,7 @@ All options sorted alphabetically:
190191
- [`--base-class-map`](model-customization.md#base-class-map) - Test --base-class-map option for model-specific base classes...
191192
- [`--capitalize-enum-members`](field-customization.md#capitalize-enum-members) - Capitalize enum member names to UPPER_CASE format.
192193
- [`--check`](general-options.md#check) - Verify generated code matches existing output without modify...
194+
- [`--class-decorators`](template-customization.md#class-decorators) - Add custom decorators to generated model classes.
193195
- [`--class-name`](model-customization.md#class-name) - Override the auto-generated class name with a custom name.
194196
- [`--collapse-reuse-models`](model-customization.md#collapse-reuse-models) - Collapse duplicate models by replacing references instead of...
195197
- [`--collapse-root-models`](model-customization.md#collapse-root-models) - Inline root model definitions instead of creating separate w...

docs/cli-reference/template-customization.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
| Option | Description |
66
|--------|-------------|
77
| [`--additional-imports`](#additional-imports) | Add custom imports to generated output files. |
8+
| [`--class-decorators`](#class-decorators) | Add custom decorators to generated model classes. |
89
| [`--custom-file-header`](#custom-file-header) | Add custom header text to the generated file. |
910
| [`--custom-file-header-path`](#custom-file-header-path) | Add custom header content from file to generated code. |
1011
| [`--custom-formatters`](#custom-formatters) | Apply custom Python code formatters to generated output. |
@@ -32,6 +33,8 @@ comma-delimited list that will be added to the generated output file. This
3233
is useful when using custom types defined in external modules (e.g.,
3334
"datetime.datetime,datetime.date,mymodule.myclass.MyCustomPythonClass").
3435

36+
**See also:** [Custom Class Decorators](../class-decorators.md)
37+
3538
!!! tip "Usage"
3639

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

108111
---
109112

113+
## `--class-decorators` {#class-decorators}
114+
115+
Add custom decorators to generated model classes.
116+
117+
The `--class-decorators` option adds custom decorators to all generated model classes.
118+
This is useful for integrating with serialization libraries like `dataclasses_json`.
119+
120+
Use with `--additional-imports` to add the required imports for the decorators.
121+
The `@` prefix is optional and will be added automatically if missing.
122+
123+
**Related:** [`--additional-imports`](template-customization.md#additional-imports), [`--output-model-type`](model-customization.md#output-model-type)
124+
125+
**See also:** [Custom Class Decorators](../class-decorators.md)
126+
127+
!!! tip "Usage"
128+
129+
```bash
130+
datamodel-codegen --input schema.json --output-model-type dataclasses.dataclass --class-decorators @dataclass_json --additional-imports dataclasses_json.dataclass_json # (1)!
131+
```
132+
133+
1. :material-arrow-left: `--class-decorators` - the option documented here
134+
135+
??? example "Examples"
136+
137+
**Input Schema:**
138+
139+
```json
140+
{
141+
"$schema": "http://json-schema.org/draft-07/schema#",
142+
"type": "object",
143+
"title": "User",
144+
"properties": {
145+
"name": {
146+
"type": "string"
147+
},
148+
"age": {
149+
"type": "integer"
150+
},
151+
"email": {
152+
"type": "string",
153+
"format": "email"
154+
}
155+
},
156+
"required": ["name", "age"]
157+
}
158+
```
159+
160+
**Output:**
161+
162+
```python
163+
# generated by datamodel-codegen:
164+
# filename: simple_frozen_test.json
165+
# timestamp: 1985-10-26T08:21:00+00:00
166+
167+
from __future__ import annotations
168+
169+
from dataclasses import dataclass
170+
171+
from dataclasses_json import dataclass_json
172+
173+
174+
@dataclass_json
175+
@dataclass
176+
class User:
177+
name: str
178+
age: int
179+
email: str | None = None
180+
```
181+
182+
---
183+
110184
## `--custom-file-header` {#custom-file-header}
111185

112186
Add custom header text to the generated file.

src/datamodel_code_generator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
415415
base_class: str = "",
416416
base_class_map: dict[str, str] | None = None,
417417
additional_imports: list[str] | None = None,
418+
class_decorators: list[str] | None = None,
418419
custom_template_dir: Path | None = None,
419420
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
420421
validation: bool = False,
@@ -678,6 +679,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
678679
base_class=base_class,
679680
base_class_map=base_class_map,
680681
additional_imports=additional_imports,
682+
class_decorators=class_decorators,
681683
custom_template_dir=custom_template_dir,
682684
extra_template_data=extra_template_data,
683685
target_python_version=target_python_version,

src/datamodel_code_generator/__main__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,21 @@ def validate_custom_formatters(cls, values: dict[str, Any]) -> dict[str, Any]:
239239
values["custom_formatters"] = custom_formatters.split(",")
240240
return values
241241

242+
@model_validator(mode="before")
243+
def validate_class_decorators(cls, values: dict[str, Any]) -> dict[str, Any]: # noqa: N805
244+
"""Validate and split class decorators, adding @ prefix if missing."""
245+
class_decorators = values.get("class_decorators")
246+
if class_decorators is not None:
247+
decorators = []
248+
for raw_decorator in class_decorators.split(","):
249+
stripped = raw_decorator.strip()
250+
if stripped:
251+
if not stripped.startswith("@"):
252+
stripped = f"@{stripped}"
253+
decorators.append(stripped)
254+
values["class_decorators"] = decorators
255+
return values
256+
242257
__validate_output_datetime_class_err: ClassVar[str] = (
243258
'`--output-datetime-class` only allows "datetime" for '
244259
f"`--output-model-type` {DataModelType.DataclassesDataclass.value}"
@@ -384,6 +399,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
384399
base_class: str = ""
385400
base_class_map: Optional[dict[str, str]] = None # noqa: UP045
386401
additional_imports: Optional[list[str]] = None # noqa: UP045
402+
class_decorators: Optional[list[str]] = None # noqa: UP045
387403
custom_template_dir: Optional[Path] = None # noqa: UP045
388404
extra_template_data: Optional[TextIOBase] = None # noqa: UP045
389405
validation: bool = False
@@ -698,6 +714,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
698714
base_class=config.base_class,
699715
base_class_map=config.base_class_map,
700716
additional_imports=config.additional_imports,
717+
class_decorators=config.class_decorators,
701718
custom_template_dir=config.custom_template_dir,
702719
validation=config.validation,
703720
field_constraints=config.field_constraints,

src/datamodel_code_generator/arguments.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,14 @@ def start_section(self, heading: str | None) -> None:
749749
type=str,
750750
default=None,
751751
)
752+
base_options.add_argument(
753+
"--class-decorators",
754+
help="Custom decorators for generated model classes (delimited list input). "
755+
'For example "@dataclass_json(letter_case=LetterCase.CAMEL)". '
756+
'The "@" prefix is optional and will be added automatically if missing.',
757+
type=str,
758+
default=None,
759+
)
752760
base_options.add_argument(
753761
"--formatters",
754762
help="Formatters for output (default: [black, isort])",

src/datamodel_code_generator/cli_options.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ class CLIOptionMeta:
184184
"--custom-file-header": CLIOptionMeta(name="--custom-file-header", category=OptionCategory.TEMPLATE),
185185
"--custom-file-header-path": CLIOptionMeta(name="--custom-file-header-path", category=OptionCategory.TEMPLATE),
186186
"--additional-imports": CLIOptionMeta(name="--additional-imports", category=OptionCategory.TEMPLATE),
187+
"--class-decorators": CLIOptionMeta(name="--class-decorators", category=OptionCategory.TEMPLATE),
187188
"--use-double-quotes": CLIOptionMeta(name="--use-double-quotes", category=OptionCategory.TEMPLATE),
188189
"--use-exact-imports": CLIOptionMeta(name="--use-exact-imports", category=OptionCategory.TEMPLATE),
189190
"--disable-appending-item-suffix": CLIOptionMeta(

src/datamodel_code_generator/parser/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
687687
base_class: str | None = None,
688688
base_class_map: dict[str, str] | None = None,
689689
additional_imports: list[str] | None = None,
690+
class_decorators: list[str] | None = None,
690691
custom_template_dir: Path | None = None,
691692
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
692693
target_python_version: PythonVersion = PythonVersionMin,
@@ -807,6 +808,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
807808
self.imports: Imports = Imports(use_exact_imports)
808809
self.use_exact_imports: bool = use_exact_imports
809810
self._append_additional_imports(additional_imports=additional_imports)
811+
self.class_decorators: list[str] = class_decorators or []
810812

811813
self.base_class: str | None = base_class
812814
self.base_class_map: dict[str, str] | None = base_class_map

src/datamodel_code_generator/parser/graphql.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def __init__( # noqa: PLR0913
110110
base_class: str | None = None,
111111
base_class_map: dict[str, str] | None = None,
112112
additional_imports: list[str] | None = None,
113+
class_decorators: list[str] | None = None,
113114
custom_template_dir: Path | None = None,
114115
extra_template_data: defaultdict[str, dict[str, Any]] | None = None,
115116
target_python_version: PythonVersion = PythonVersionMin,
@@ -214,6 +215,7 @@ def __init__( # noqa: PLR0913
214215
base_class=base_class,
215216
base_class_map=base_class_map,
216217
additional_imports=additional_imports,
218+
class_decorators=class_decorators,
217219
custom_template_dir=custom_template_dir,
218220
extra_template_data=extra_template_data,
219221
target_python_version=target_python_version,
@@ -362,6 +364,9 @@ def _resolve_types(self, paths: list[str], schema: graphql.GraphQLSchema) -> Non
362364

363365
def _create_data_model(self, model_type: type[DataModel] | None = None, **kwargs: Any) -> DataModel:
364366
"""Create data model instance with dataclass_arguments support for DataClass."""
367+
# Add class decorators if not already provided
368+
if "decorators" not in kwargs and self.class_decorators:
369+
kwargs["decorators"] = list(self.class_decorators)
365370
data_model_class = model_type or self.data_model_type
366371
if issubclass(data_model_class, (DataClass, PydanticV2DataClass)):
367372
# Use dataclass_arguments from kwargs, or fall back to self.dataclass_arguments

0 commit comments

Comments
 (0)