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
3 changes: 2 additions & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This documentation is auto-generated from test cases.
| 📁 [Base Options](base-options.md) | 5 | Input/output configuration |
| 🔧 [Typing Customization](typing-customization.md) | 17 | Type annotation and import behavior |
| 🏷️ [Field Customization](field-customization.md) | 20 | Field naming and docstring behavior |
| 🏗️ [Model Customization](model-customization.md) | 26 | Model generation behavior |
| 🏗️ [Model Customization](model-customization.md) | 27 | Model generation behavior |
| 🎨 [Template Customization](template-customization.md) | 16 | 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 |
Expand Down Expand Up @@ -160,6 +160,7 @@ This documentation is auto-generated from test cases.
- [`--use-attribute-docstrings`](field-customization.md#use-attribute-docstrings)
- [`--use-decimal-for-multiple-of`](typing-customization.md#use-decimal-for-multiple-of)
- [`--use-default`](model-customization.md#use-default)
- [`--use-default-factory-for-optional-nested-models`](model-customization.md#use-default-factory-for-optional-nested-models)
- [`--use-default-kwarg`](model-customization.md#use-default-kwarg)
- [`--use-double-quotes`](template-customization.md#use-double-quotes)
- [`--use-enum-values-in-discriminator`](field-customization.md#use-enum-values-in-discriminator)
Expand Down
143 changes: 143 additions & 0 deletions docs/cli-reference/model-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
| [`--target-python-version`](#target-python-version) | Target Python version for generated code syntax and imports.... |
| [`--union-mode`](#union-mode) | Union mode for combining anyOf/oneOf schemas (smart or left_... |
| [`--use-default`](#use-default) | Use default values from schema in generated models. |
| [`--use-default-factory-for-optional-nested-models`](#use-default-factory-for-optional-nested-models) | Generate default_factory for optional nested model fields. |
| [`--use-default-kwarg`](#use-default-kwarg) | Use default= keyword argument instead of positional argument... |
| [`--use-frozen-field`](#use-frozen-field) | Generate frozen (immutable) field definitions for readOnly p... |
| [`--use-one-literal-as-default`](#use-one-literal-as-default) | Use single literal value as default when enum has only one o... |
Expand Down Expand Up @@ -4834,6 +4835,148 @@ The `--use-default` flag configures the code generation behavior.

---

## `--use-default-factory-for-optional-nested-models` {#use-default-factory-for-optional-nested-models}

Generate default_factory for optional nested model fields.

The `--use-default-factory-for-optional-nested-models` flag generates default_factory
for optional nested model fields instead of None default:
- Dataclasses: `field: Model | None = field(default_factory=Model)`
- Pydantic: `field: Model | None = Field(default_factory=Model)`
- msgspec: `field: Model | UnsetType = field(default_factory=Model)`

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --use-default-factory-for-optional-nested-models # (1)!
```

1. :material-arrow-left: `--use-default-factory-for-optional-nested-models` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"$ref": "#/$defs/Address"},
"contact": {"$ref": "#/$defs/Contact"}
},
"required": ["name"],
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
}
},
"Contact": {
"type": "object",
"properties": {
"email": {"type": "string"},
"phone": {"type": "string"}
}
}
}
}
```

**Output:**

=== "Pydantic v2"

```python
# generated by datamodel-codegen:
# filename: default_factory_nested_model.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel, Field


class Address(BaseModel):
street: str | None = None
city: str | None = None


class Contact(BaseModel):
email: str | None = None
phone: str | None = None


class Model(BaseModel):
name: str
address: Address | None = Field(default_factory=Address)
contact: Contact | None = Field(default_factory=Contact)
```

=== "dataclass"

```python
# generated by datamodel-codegen:
# filename: default_factory_nested_model.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from dataclasses import dataclass, field


@dataclass
class Address:
street: str | None = None
city: str | None = None


@dataclass
class Contact:
email: str | None = None
phone: str | None = None


@dataclass
class Model:
name: str
address: Address | None = field(default_factory=Address)
contact: Contact | None = field(default_factory=Contact)
```

=== "msgspec"

```python
# generated by datamodel-codegen:
# filename: default_factory_nested_model.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from msgspec import UNSET, Struct, UnsetType, field


class Address(Struct):
street: str | UnsetType = UNSET
city: str | UnsetType = UNSET


class Contact(Struct):
email: str | UnsetType = UNSET
phone: str | UnsetType = UNSET


class Model(Struct):
name: str
address: Address | UnsetType = field(default_factory=Address)
contact: Contact | UnsetType = field(default_factory=Contact)
```

---

## `--use-default-kwarg` {#use-default-kwarg}

Use default= keyword argument instead of positional argument for fields with defaults.
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 @@ -94,6 +94,7 @@ datamodel-codegen [OPTIONS]
| [`--target-python-version`](model-customization.md#target-python-version) | Target Python version for generated code syntax and imports. |
| [`--union-mode`](model-customization.md#union-mode) | Union mode for combining anyOf/oneOf schemas (smart or left_to_right). |
| [`--use-default`](model-customization.md#use-default) | Use default values from schema in generated models. |
| [`--use-default-factory-for-optional-nested-models`](model-customization.md#use-default-factory-for-optional-nested-models) | Generate default_factory for optional nested model fields. |
| [`--use-default-kwarg`](model-customization.md#use-default-kwarg) | Use default= keyword argument instead of positional argument for fields with def... |
| [`--use-frozen-field`](model-customization.md#use-frozen-field) | Generate frozen (immutable) field definitions for readOnly properties. |
| [`--use-one-literal-as-default`](model-customization.md#use-one-literal-as-default) | Use single literal value as default when enum has only one option. |
Expand Down Expand Up @@ -252,6 +253,7 @@ All options sorted alphabetically:
- [`--use-attribute-docstrings`](field-customization.md#use-attribute-docstrings) - Generate field descriptions as attribute docstrings instead ...
- [`--use-decimal-for-multiple-of`](typing-customization.md#use-decimal-for-multiple-of) - Generate Decimal types for fields with multipleOf constraint...
- [`--use-default`](model-customization.md#use-default) - Use default values from schema in generated models.
- [`--use-default-factory-for-optional-nested-models`](model-customization.md#use-default-factory-for-optional-nested-models) - Generate default_factory for optional nested model fields.
- [`--use-default-kwarg`](model-customization.md#use-default-kwarg) - Use default= keyword argument instead of positional argument...
- [`--use-double-quotes`](template-customization.md#use-double-quotes) - Use double quotes for string literals in generated code.
- [`--use-enum-values-in-discriminator`](field-customization.md#use-enum-values-in-discriminator) - Use enum values in discriminator mappings for union types.
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 @@ -470,6 +470,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
frozen_dataclasses: bool = False,
no_alias: bool = False,
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
formatters: list[Formatter] = DEFAULT_FORMATTERS,
settings_path: Path | None = None,
parent_scoped_naming: bool = False,
Expand Down Expand Up @@ -717,6 +718,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
frozen_dataclasses=frozen_dataclasses,
no_alias=no_alias,
use_frozen_field=use_frozen_field,
use_default_factory_for_optional_nested_models=use_default_factory_for_optional_nested_models,
formatters=formatters,
encoding=encoding,
parent_scoped_naming=parent_scoped_naming,
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
dataclass_arguments: Optional[DataclassArguments] = None # noqa: UP045
no_alias: bool = False
use_frozen_field: bool = False
use_default_factory_for_optional_nested_models: bool = False
formatters: list[Formatter] = DEFAULT_FORMATTERS
parent_scoped_naming: bool = False
disable_future_imports: bool = False
Expand Down Expand Up @@ -761,6 +762,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
frozen_dataclasses=config.frozen_dataclasses,
no_alias=config.no_alias,
use_frozen_field=config.use_frozen_field,
use_default_factory_for_optional_nested_models=config.use_default_factory_for_optional_nested_models,
formatters=config.formatters,
settings_path=settings_path,
parent_scoped_naming=config.parent_scoped_naming,
Expand Down
7 changes: 7 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,13 @@ def start_section(self, heading: str | None) -> None:
action="store_true",
default=None,
)
field_options.add_argument(
"--use-default-factory-for-optional-nested-models",
help="Use default_factory for optional nested model fields instead of None default. "
"E.g., `field: Model | None = Field(default_factory=Model)` instead of `field: Model | None = None`",
action="store_true",
default=None,
)

# ======================================================================================
# Options for templating output
Expand Down
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class CLIOptionMeta:
"--strip-default-none": CLIOptionMeta(name="--strip-default-none", category=OptionCategory.MODEL),
"--dataclass-arguments": CLIOptionMeta(name="--dataclass-arguments", category=OptionCategory.MODEL),
"--use-frozen-field": CLIOptionMeta(name="--use-frozen-field", category=OptionCategory.MODEL),
"--use-default-factory-for-optional-nested-models": CLIOptionMeta(
name="--use-default-factory-for-optional-nested-models", category=OptionCategory.MODEL
),
"--union-mode": CLIOptionMeta(name="--union-mode", category=OptionCategory.MODEL),
"--parent-scoped-naming": CLIOptionMeta(name="--parent-scoped-naming", category=OptionCategory.MODEL),
"--use-one-literal-as-default": CLIOptionMeta(name="--use-one-literal-as-default", category=OptionCategory.MODEL),
Expand Down
1 change: 1 addition & 0 deletions src/datamodel_code_generator/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ class Config:
read_only: bool = False
write_only: bool = False
use_frozen_field: bool = False
use_default_factory_for_optional_nested_models: bool = False

if not TYPE_CHECKING:
if not PYDANTIC_V2:
Expand Down
23 changes: 23 additions & 0 deletions src/datamodel_code_generator/model/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ def field(self) -> str | None:
return None
return result

def _get_default_factory_for_nested_model(self) -> str | None:
"""Get default_factory for nested dataclass model fields.

Returns the class name if the field type references a DataClass,
otherwise returns None.
"""
for data_type in self.data_type.data_types or (self.data_type,):
if data_type.is_dict:
continue
if data_type.reference and isinstance(data_type.reference.source, DataClass):
return data_type.alias or data_type.reference.source.class_name
return None

def __str__(self) -> str:
"""Generate field() call or default value representation."""
data: dict[str, Any] = {k: v for k, v in self.extras.items() if k in self._FIELD_KEYS}
Expand All @@ -161,6 +174,16 @@ def __str__(self) -> str:
}
}

if (
self.use_default_factory_for_optional_nested_models
and not self.required
and (self.default is None or self.default is UNDEFINED)
and "default_factory" not in data
):
nested_model_name = self._get_default_factory_for_nested_model()
if nested_model_name:
data["default_factory"] = nested_model_name

if not data:
return ""

Expand Down
26 changes: 25 additions & 1 deletion src/datamodel_code_generator/model/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ def field(self) -> str | None:
return None
return result

def __str__(self) -> str:
def __str__(self) -> str: # noqa: PLR0912
"""Generate field() call or default value representation."""
data: dict[str, Any] = {k: v for k, v in self.extras.items() if k in self._FIELD_KEYS}
if self.alias:
Expand Down Expand Up @@ -284,6 +284,17 @@ def __str__(self) -> str:
else:
data["default_factory"] = type(default_value).__name__

if (
self.use_default_factory_for_optional_nested_models
and not self.required
and (self.default is None or self.default is UNDEFINED)
and "default_factory" not in data
):
nested_model_name = self._get_default_factory_for_optional_nested_model()
if nested_model_name:
data["default_factory"] = nested_model_name
data.pop("default", None)

if not data:
return ""

Expand Down Expand Up @@ -412,6 +423,19 @@ def _get_default_as_struct_model(self) -> str | None:
)
return None

def _get_default_factory_for_optional_nested_model(self) -> str | None:
"""Get default_factory for optional nested Struct model fields.

Returns the class name if the field type references a Struct,
otherwise returns None.
"""
for data_type in self.data_type.data_types or (self.data_type,):
if data_type.is_dict:
continue
if data_type.reference and isinstance(data_type.reference.source, Struct):
return data_type.alias or data_type.reference.source.class_name
return None


class DataTypeManager(_DataTypeManager):
"""Type manager for msgspec Struct models."""
Expand Down
21 changes: 21 additions & 0 deletions src/datamodel_code_generator/model/pydantic/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ def _get_default_as_pydantic_model(self) -> str | None:
)
return None

def _get_default_factory_for_optional_nested_model(self) -> str | None:
"""Get default_factory for optional nested Pydantic model fields.

Returns the class name if the field type references a BaseModel,
otherwise returns None.
"""
for data_type in self.data_type.data_types or (self.data_type,):
if data_type.is_dict:
continue
if data_type.reference and isinstance(data_type.reference.source, BaseModelBase):
return data_type.alias or data_type.reference.source.class_name
return None

def _process_data_in_str(self, data: dict[str, Any]) -> None:
if self.const:
data["const"] = True
Expand Down Expand Up @@ -204,6 +217,14 @@ def __str__(self) -> str: # noqa: PLR0912
else:
default_factory = data.pop("default_factory", None)

if (
default_factory is None
and self.use_default_factory_for_optional_nested_models
and not self.required
and (self.default is None or self.default is UNDEFINED)
):
default_factory = self._get_default_factory_for_optional_nested_model()

self.__dict__["_computed_default_factory"] = default_factory

field_arguments = sorted(f"{k}={v!r}" for k, v in data.items() if v is not None)
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 @@ -734,6 +734,7 @@ def __init__( # noqa: PLR0913, PLR0915
frozen_dataclasses: bool = False,
no_alias: bool = False,
use_frozen_field: bool = False,
use_default_factory_for_optional_nested_models: bool = False,
formatters: list[Formatter] = DEFAULT_FORMATTERS,
parent_scoped_naming: bool = False,
dataclass_arguments: DataclassArguments | None = None,
Expand Down Expand Up @@ -875,6 +876,7 @@ def __init__( # noqa: PLR0913, PLR0915
self.type_mappings: dict[tuple[str, str], str] = Parser._parse_type_mappings(type_mappings)
self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = read_only_write_only_model_type
self.use_frozen_field: bool = use_frozen_field
self.use_default_factory_for_optional_nested_models: bool = use_default_factory_for_optional_nested_models

@property
def field_name_model_type(self) -> ModelType:
Expand Down
Loading
Loading