diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index d481179dc..34f1d7b89 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -11,7 +11,7 @@ This documentation is auto-generated from test cases. | 📁 [Base Options](base-options.md) | 10 | Input/output configuration | | 🔧 [Typing Customization](typing-customization.md) | 29 | Type annotation and import behavior | | 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior | -| 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior | +| 🏗️ [Model Customization](model-customization.md) | 40 | Model generation behavior | | 🎨 [Template Customization](template-customization.md) | 21 | Output formatting and custom rendering | | 📘 [OpenAPI-only Options](openapi-only-options.md) | 7 | OpenAPI-specific features | | 📋 [GraphQL-only Options](graphql-only-options.md) | 1 | | @@ -194,6 +194,7 @@ This documentation is auto-generated from test cases. - [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) - [`--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) diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index e0ce2d726..1c00ee367 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -37,6 +37,7 @@ | [`--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-keep-required-non-nullable`](#use-default-keep-required-non-nullable) | When using default, keep required field non nullable/non-opt... | | [`--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-generic-base-class`](#use-generic-base-class) | Generate a shared base class with model configuration to avo... | @@ -5292,38 +5293,142 @@ with their defaults, making them optional to provide when instantiating the mode ??? example "Examples" - **Input Schema:** + === "OpenAPI" - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Use default with const", - "properties": { - "foo": { - "const": "foo" + **Input Schema:** + + ```yaml + openapi: 3.1.0 + info: + title: Data test + description: Data test + components: + schemas: + data: + type: object + required: + - number + properties: + number: + description: Number + default: 1 + type: integer + format: int32 + minimum: 0 + block: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/data' + dataOptional: + $ref: '#/components/schemas/data' + ``` + + **Output:** + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field, conint + + + class Data(BaseModel): + number: conint(ge=0) = Field(1, description='Number') + + + class Block(BaseModel): + data: Data + dataOptional: Data | None = None + ``` + + === "dataclass" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from dataclasses import dataclass + + + @dataclass + class Data: + number: int = 1 + + + @dataclass + class Block: + data: Data + dataOptional: Data | None = None + ``` + + === "msgspec" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Annotated + + from msgspec import UNSET, Meta, Struct, UnsetType + + + class Data(Struct): + number: Annotated[int, Meta(description='Number', ge=0)] = 1 + + + class Block(Struct): + data: Data + dataOptional: Data | UnsetType = UNSET + ``` + + === "JSON Schema" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Use default with const", + "properties": { + "foo": { + "const": "foo" + } } } - } - ``` + ``` - **Output:** + **Output:** - ```python - # generated by datamodel-codegen: - # filename: use_default_with_const.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Literal - - from pydantic import BaseModel - - - class UseDefaultWithConst(BaseModel): - foo: Literal['foo'] = 'foo' - ``` + ```python + # generated by datamodel-codegen: + # filename: use_default_with_const.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Literal + + from pydantic import BaseModel + + + class UseDefaultWithConst(BaseModel): + foo: Literal['foo'] = 'foo' + ``` --- @@ -5469,6 +5574,130 @@ for optional nested model fields instead of None default: --- +## `--use-default-keep-required-non-nullable` {#use-default-keep-required-non-nullable} + +When using default, keep required field non nullable/non-optionaloptional. + +The `--use-default-keep-required-non-nullable` flag keeps a required field +as non-nullable/optional: +- Dataclasses: `field: int = 1` +- Pydantic: `field: int = 1` +- msgspec: `field: int = 1` + +**Related:** [`--use-default`](model-customization.md#use-default) + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --use-default --use-default-keep-required-non-nullable # (1)! + ``` + + 1. :material-arrow-left: `--use-default-keep-required-non-nullable` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```yaml + openapi: 3.1.0 + info: + title: Data test + description: Data test + components: + schemas: + data: + type: object + required: + - number + properties: + number: + description: Number + default: 1 + type: integer + format: int32 + minimum: 0 + block: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/data' + dataOptional: + $ref: '#/components/schemas/data' + ``` + + **Output:** + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field, conint + + + class Data(BaseModel): + number: conint(ge=0) = Field(1, description='Number') + + + class Block(BaseModel): + data: Data + dataOptional: Data | None = None + ``` + + === "dataclass" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from dataclasses import dataclass + + + @dataclass + class Data: + number: int = 1 + + + @dataclass + class Block: + data: Data + dataOptional: Data | None = None + ``` + + === "msgspec" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Annotated + + from msgspec import UNSET, Meta, Struct, UnsetType + + + class Data(Struct): + number: Annotated[int, Meta(description='Number', ge=0)] = 1 + + + class Block(Struct): + data: Data + dataOptional: Data | UnsetType = UNSET + ``` + +--- + ## `--use-default-kwarg` {#use-default-kwarg} Use default= keyword argument instead of positional argument for fields with defaults. diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index 1308bcc66..094e60c10 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -127,6 +127,7 @@ datamodel-codegen [OPTIONS] | [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) | When using default, keep required field non nullable/non-optionaloptional. | | [`--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-generic-base-class`](model-customization.md#use-generic-base-class) | Generate a shared base class with model configuration to avoid repetition (DRY). | @@ -334,6 +335,7 @@ All options sorted alphabetically: - [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) - When using default, keep required field non nullable/non-opt... - [`--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. diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f4226185b..6020c5cfa 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -246,7 +246,7 @@ This documentation is auto-generated from test cases. | 📁 [Base Options](base-options.md) | 10 | Input/output configuration | | 🔧 [Typing Customization](typing-customization.md) | 29 | Type annotation and import behavior | | 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior | -| 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior | +| 🏗️ [Model Customization](model-customization.md) | 40 | Model generation behavior | | 🎨 [Template Customization](template-customization.md) | 21 | Output formatting and custom rendering | | 📘 [OpenAPI-only Options](openapi-only-options.md) | 7 | OpenAPI-specific features | | 📋 [GraphQL-only Options](graphql-only-options.md) | 1 | | @@ -429,6 +429,7 @@ This documentation is auto-generated from test cases. - [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) - [`--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) @@ -1268,6 +1269,7 @@ Source: https://datamodel-code-generator.koxudaxi.dev/cli-reference/model-custom | [`--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-keep-required-non-nullable`](#use-default-keep-required-non-nullable) | When using default, keep required field non nullable/non-opt... | | [`--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-generic-base-class`](#use-generic-base-class) | Generate a shared base class with model configuration to avo... | @@ -6523,38 +6525,142 @@ with their defaults, making them optional to provide when instantiating the mode ??? example "Examples" - **Input Schema:** + === "OpenAPI" - ```json - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "title": "Use default with const", - "properties": { - "foo": { - "const": "foo" + **Input Schema:** + + ```yaml + openapi: 3.1.0 + info: + title: Data test + description: Data test + components: + schemas: + data: + type: object + required: + - number + properties: + number: + description: Number + default: 1 + type: integer + format: int32 + minimum: 0 + block: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/data' + dataOptional: + $ref: '#/components/schemas/data' + ``` + + **Output:** + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field, conint + + + class Data(BaseModel): + number: conint(ge=0) = Field(1, description='Number') + + + class Block(BaseModel): + data: Data + dataOptional: Data | None = None + ``` + + === "dataclass" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from dataclasses import dataclass + + + @dataclass + class Data: + number: int = 1 + + + @dataclass + class Block: + data: Data + dataOptional: Data | None = None + ``` + + === "msgspec" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Annotated + + from msgspec import UNSET, Meta, Struct, UnsetType + + + class Data(Struct): + number: Annotated[int, Meta(description='Number', ge=0)] = 1 + + + class Block(Struct): + data: Data + dataOptional: Data | UnsetType = UNSET + ``` + + === "JSON Schema" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "Use default with const", + "properties": { + "foo": { + "const": "foo" + } } } - } - ``` + ``` - **Output:** + **Output:** - ```python - # generated by datamodel-codegen: - # filename: use_default_with_const.json - # timestamp: 2019-07-26T00:00:00+00:00 - - from __future__ import annotations - - from typing import Literal - - from pydantic import BaseModel - - - class UseDefaultWithConst(BaseModel): - foo: Literal['foo'] = 'foo' - ``` + ```python + # generated by datamodel-codegen: + # filename: use_default_with_const.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Literal + + from pydantic import BaseModel + + + class UseDefaultWithConst(BaseModel): + foo: Literal['foo'] = 'foo' + ``` --- @@ -6700,6 +6806,130 @@ for optional nested model fields instead of None default: --- +## `--use-default-keep-required-non-nullable` {#use-default-keep-required-non-nullable} + +When using default, keep required field non nullable/non-optionaloptional. + +The `--use-default-keep-required-non-nullable` flag keeps a required field +as non-nullable/optional: +- Dataclasses: `field: int = 1` +- Pydantic: `field: int = 1` +- msgspec: `field: int = 1` + +**Related:** [`--use-default`](model-customization.md#use-default) + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --use-default --use-default-keep-required-non-nullable # (1)! + ``` + + 1. :material-arrow-left: `--use-default-keep-required-non-nullable` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```yaml + openapi: 3.1.0 + info: + title: Data test + description: Data test + components: + schemas: + data: + type: object + required: + - number + properties: + number: + description: Number + default: 1 + type: integer + format: int32 + minimum: 0 + block: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/data' + dataOptional: + $ref: '#/components/schemas/data' + ``` + + **Output:** + + === "Pydantic v2" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field, conint + + + class Data(BaseModel): + number: conint(ge=0) = Field(1, description='Number') + + + class Block(BaseModel): + data: Data + dataOptional: Data | None = None + ``` + + === "dataclass" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from dataclasses import dataclass + + + @dataclass + class Data: + number: int = 1 + + + @dataclass + class Block: + data: Data + dataOptional: Data | None = None + ``` + + === "msgspec" + + ```python + # generated by datamodel-codegen: + # filename: default_factory_keep_required_non_nullable.yaml + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from typing import Annotated + + from msgspec import UNSET, Meta, Struct, UnsetType + + + class Data(Struct): + number: Annotated[int, Meta(description='Number', ge=0)] = 1 + + + class Block(Struct): + data: Data + dataOptional: Data | UnsetType = UNSET + ``` + +--- + ## `--use-default-kwarg` {#use-default-kwarg} Use default= keyword argument instead of positional argument for fields with defaults. @@ -24032,6 +24262,7 @@ datamodel-codegen [OPTIONS] | [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) | When using default, keep required field non nullable/non-optionaloptional. | | [`--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-generic-base-class`](model-customization.md#use-generic-base-class) | Generate a shared base class with model configuration to avoid repetition (DRY). | @@ -24239,6 +24470,7 @@ All options sorted alphabetically: - [`--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-keep-required-non-nullable`](model-customization.md#use-default-keep-required-non-nullable) - When using default, keep required field non nullable/non-opt... - [`--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. diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index e7225ea13..686b5bfd9 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -531,6 +531,7 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) -> use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False + use_default_keep_required_non_nullable: bool = False formatters: list[Formatter] | None = None parent_scoped_naming: bool = False naming_strategy: Optional[NamingStrategy] = None # noqa: UP045 @@ -982,6 +983,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 use_serialization_alias=config.use_serialization_alias, use_frozen_field=config.use_frozen_field, use_default_factory_for_optional_nested_models=config.use_default_factory_for_optional_nested_models, + use_default_keep_required_non_nullable=config.use_default_keep_required_non_nullable, formatters=config.formatters, settings_path=settings_path, parent_scoped_naming=config.parent_scoped_naming, diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index c0c278d10..750a06a23 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -156,6 +156,7 @@ class GenerateConfigDict(TypedDict, closed=True): use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] + use_default_keep_required_non_nullable: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] settings_path: NotRequired[Path | None] parent_scoped_naming: NotRequired[bool] diff --git a/src/datamodel_code_generator/_types/parser_config_dicts.py b/src/datamodel_code_generator/_types/parser_config_dicts.py index c99912474..9224191e9 100644 --- a/src/datamodel_code_generator/_types/parser_config_dicts.py +++ b/src/datamodel_code_generator/_types/parser_config_dicts.py @@ -152,6 +152,7 @@ class ParserConfigDict(TypedDict): use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] + use_default_keep_required_non_nullable: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] defer_formatting: NotRequired[bool] parent_scoped_naming: NotRequired[bool] diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index 7c8f7a7a0..466f22237 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -886,6 +886,13 @@ def start_section(self, heading: str | None) -> None: action="store_true", default=None, ) +field_options.add_argument( + "--use-default-keep-required-non-nullable", + help="When using default, keep required field non nullable/non-optionaloptional." + "E.g., `field: int = 1` instead of `field: Model | None = Field(...)`", + action="store_true", + default=None, +) field_options.add_argument( "--field-type-collision-strategy", help="Strategy for handling field name and type name collisions (Pydantic v2 only). " diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index ad64860d3..6c56127f7 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -111,6 +111,9 @@ class CLIOptionMeta: "--use-default-factory-for-optional-nested-models": CLIOptionMeta( name="--use-default-factory-for-optional-nested-models", category=OptionCategory.MODEL ), + "--use-default-keep-required-non-nullable": CLIOptionMeta( + name="--use-default-keep-required-non-nullable", category=OptionCategory.MODEL + ), "--union-mode": CLIOptionMeta(name="--union-mode", category=OptionCategory.MODEL), "--parent-scoped-naming": CLIOptionMeta( name="--parent-scoped-naming", diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index 4bd1ed9a3..41d6181c0 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -181,6 +181,7 @@ class GenerateConfig(BaseModel): use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False + use_default_keep_required_non_nullable: bool = False formatters: list[Formatter] | None = None settings_path: Path | None = None parent_scoped_naming: bool = False @@ -315,6 +316,7 @@ class ParserConfig(BaseModel): use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False + use_default_keep_required_non_nullable: bool = False formatters: list[Formatter] | None = None defer_formatting: bool = False parent_scoped_naming: bool = False diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index 7110f58bc..1cb6dbc6f 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -173,6 +173,7 @@ class DataModelFieldBase(_BaseModel): use_frozen_field: bool = False use_serialization_alias: bool = False use_default_factory_for_optional_nested_models: bool = False + use_default_keep_required_non_nullable: bool = False if not TYPE_CHECKING: # pragma: no branch diff --git a/src/datamodel_code_generator/model/dataclass.py b/src/datamodel_code_generator/model/dataclass.py index 00040d6d8..befc5d50d 100644 --- a/src/datamodel_code_generator/model/dataclass.py +++ b/src/datamodel_code_generator/model/dataclass.py @@ -172,7 +172,7 @@ def __str__(self) -> str: if self.default != UNDEFINED and self.default is not None: data["default"] = self.default - if self.required: + if self.required and not (self.has_default and self.use_default_keep_required_non_nullable): data = { k: v for k, v in data.items() diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index 2119f41d9..4e8ec0e90 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -311,7 +311,7 @@ def __str__(self) -> str: # noqa: PLR0912 elif self._not_required and "default_factory" not in data: data["default"] = None if self.nullable else UNSET - if self.required: + if self.required and not (self.has_default and self.use_default_keep_required_non_nullable): data = { k: v for k, v in data.items() diff --git a/src/datamodel_code_generator/model/pydantic_base.py b/src/datamodel_code_generator/model/pydantic_base.py index 9c359afb7..0b0b37e8e 100644 --- a/src/datamodel_code_generator/model/pydantic_base.py +++ b/src/datamodel_code_generator/model/pydantic_base.py @@ -204,6 +204,8 @@ def __str__(self) -> str: # noqa: PLR0912 if not field_arguments and not default_factory: if self.nullable and self.required: return "Field(...)" # Field() is for mypy + if self.required and self.has_default and self.use_default_keep_required_non_nullable: + return repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) return "" if default_factory: @@ -211,7 +213,12 @@ def __str__(self) -> str: # noqa: PLR0912 if self.use_annotated: field_arguments = self._process_annotated_field_arguments(field_arguments) - elif self.required and not default_factory and not self.extras.get("validate_default"): + elif ( + self.required + and not default_factory + and not self.extras.get("validate_default") + and not (self.has_default and self.use_default_keep_required_non_nullable) + ): field_arguments = ["...", *field_arguments] elif not default_factory: default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) @@ -246,7 +253,7 @@ def annotated(self) -> str | None: @property def imports(self) -> tuple[Import, ...]: """Get all required imports including Field if needed.""" - if self.field: + if self.field and self.field.startswith("Field("): return chain_as_tuple(super().imports, (IMPORT_FIELD,)) return super().imports diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index 73eb59d4f..73e8c049a 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -1144,6 +1144,7 @@ def __init__( # noqa: PLR0912, PLR0915 self.use_default_factory_for_optional_nested_models: bool = ( config.use_default_factory_for_optional_nested_models ) + self.use_default_keep_required_non_nullable: bool = config.use_default_keep_required_non_nullable self.field_type_collision_strategy: FieldTypeCollisionStrategy | None = config.field_type_collision_strategy @property diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 82f7e7051..8fb7e57f5 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -372,7 +372,11 @@ def parse_field( class_name=class_name, ) - if self.apply_default_values_for_required_fields and effective_has_default: + if ( + self.apply_default_values_for_required_fields + and effective_has_default + and not self.use_default_keep_required_non_nullable + ): required = False extras = {} if self.default_field_extras is None else self.default_field_extras.copy() diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 34ee95f3c..8b1111934 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -694,10 +694,8 @@ def __init__( self._circular_ref_cache: dict[str, bool] = {} if self.data_model_field_type.can_have_extra_keys: - self.get_field_extra_key: Callable[[str], str] = ( - lambda key: self.model_resolver.get_valid_field_name_and_alias( - key, model_type=self.field_name_model_type - )[0] + self.get_field_extra_key: Callable[[str], str] = lambda key: ( + self.model_resolver.get_valid_field_name_and_alias(key, model_type=self.field_name_model_type)[0] ) else: @@ -1252,6 +1250,7 @@ def get_object_field( # noqa: PLR0913 use_frozen_field=self.use_frozen_field, use_serialization_alias=self.use_serialization_alias, use_default_factory_for_optional_nested_models=self.use_default_factory_for_optional_nested_models, + use_default_keep_required_non_nullable=self.use_default_keep_required_non_nullable, ) def get_data_type(self, obj: JsonSchemaObject) -> DataType: @@ -2830,11 +2829,14 @@ def parse_object_fields( ) if self.force_optional_for_required_fields or ( - self.apply_default_values_for_required_fields and effective_has_default + self.apply_default_values_for_required_fields + and effective_has_default + and not self.use_default_keep_required_non_nullable ): required: bool = False else: required = original_field_name in requires + fields.append( self.get_object_field( field_name=field_name, diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 2e30a46b1..be994e44a 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -551,7 +551,11 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 class_name=reference.name, ) effective_required = parameter.required - if self.apply_default_values_for_required_fields and effective_has_default: + if ( + self.apply_default_values_for_required_fields + and effective_has_default + and not self.use_default_keep_required_non_nullable + ): effective_required = False fields.append( self.get_object_field( @@ -600,7 +604,11 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914, PLR0915 class_name=reference.name, ) effective_required = parameter.required - if self.apply_default_values_for_required_fields and effective_has_default: + if ( + self.apply_default_values_for_required_fields + and effective_has_default + and not self.use_default_keep_required_non_nullable + ): effective_required = False # Handle multiple aliases (Pydantic v2 AliasChoices) single_alias: str | None = None diff --git a/src/datamodel_code_generator/prompt_data.py b/src/datamodel_code_generator/prompt_data.py index 32fae3512..4a7190d71 100644 --- a/src/datamodel_code_generator/prompt_data.py +++ b/src/datamodel_code_generator/prompt_data.py @@ -120,6 +120,7 @@ "--use-decimal-for-multiple-of": "Generate Decimal types for fields with multipleOf constraint.", "--use-default": "Use default values from schema in generated models.", "--use-default-factory-for-optional-nested-models": "Generate default_factory for optional nested model fields.", + "--use-default-keep-required-non-nullable": "When using default, keep required field non nullable/non-optionaloptional.", "--use-default-kwarg": "Use default= keyword argument instead of positional argument for fields with defaults.", "--use-double-quotes": "Use double quotes for string literals in generated code.", "--use-enum-values-in-discriminator": "Use enum values in discriminator mappings for union types.", diff --git a/tests/data/expected/main/input_model/config_class.py b/tests/data/expected/main/input_model/config_class.py index 02437ce39..0472b03f5 100644 --- a/tests/data/expected/main/input_model/config_class.py +++ b/tests/data/expected/main/input_model/config_class.py @@ -235,6 +235,7 @@ class GenerateConfig(TypedDict, closed=True): use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] + use_default_keep_required_non_nullable: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] settings_path: NotRequired[str | None] parent_scoped_naming: NotRequired[bool] diff --git a/tests/data/expected/main/openapi/default_keep_required_non_nullable_dataclass.py b/tests/data/expected/main/openapi/default_keep_required_non_nullable_dataclass.py new file mode 100644 index 000000000..f0307030d --- /dev/null +++ b/tests/data/expected/main/openapi/default_keep_required_non_nullable_dataclass.py @@ -0,0 +1,18 @@ +# generated by datamodel-codegen: +# filename: default_factory_keep_required_non_nullable.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Data: + number: int = 1 + + +@dataclass +class Block: + data: Data + dataOptional: Data | None = None diff --git a/tests/data/expected/main/openapi/default_keep_required_non_nullable_msgspec.py b/tests/data/expected/main/openapi/default_keep_required_non_nullable_msgspec.py new file mode 100644 index 000000000..0723985b3 --- /dev/null +++ b/tests/data/expected/main/openapi/default_keep_required_non_nullable_msgspec.py @@ -0,0 +1,18 @@ +# generated by datamodel-codegen: +# filename: default_factory_keep_required_non_nullable.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Annotated + +from msgspec import UNSET, Meta, Struct, UnsetType + + +class Data(Struct): + number: Annotated[int, Meta(description='Number', ge=0)] = 1 + + +class Block(Struct): + data: Data + dataOptional: Data | UnsetType = UNSET diff --git a/tests/data/expected/main/openapi/default_keep_required_non_nullable_pydantic_v2.py b/tests/data/expected/main/openapi/default_keep_required_non_nullable_pydantic_v2.py new file mode 100644 index 000000000..a3bab398c --- /dev/null +++ b/tests/data/expected/main/openapi/default_keep_required_non_nullable_pydantic_v2.py @@ -0,0 +1,16 @@ +# generated by datamodel-codegen: +# filename: default_factory_keep_required_non_nullable.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel, Field, conint + + +class Data(BaseModel): + number: conint(ge=0) = Field(1, description='Number') + + +class Block(BaseModel): + data: Data + dataOptional: Data | None = None diff --git a/tests/data/expected/main/openapi/use_default_keep_required_non_nullable.py b/tests/data/expected/main/openapi/use_default_keep_required_non_nullable.py new file mode 100644 index 000000000..d8f4c79aa --- /dev/null +++ b/tests/data/expected/main/openapi/use_default_keep_required_non_nullable.py @@ -0,0 +1,67 @@ +# generated by datamodel-codegen: +# filename: api.yaml +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import AnyUrl, BaseModel, Field, RootModel + + +class Pet(BaseModel): + id: int = 1 + name: str + tag: str | None = None + + +class Pets(RootModel[list[Pet]]): + root: list[Pet] + + +class User(BaseModel): + id: int + name: str + tag: str | None = None + + +class Users(RootModel[list[User]]): + root: list[User] + + +class Id(RootModel[str]): + root: str + + +class Rules(RootModel[list[str]]): + root: list[str] + + +class Error(BaseModel): + code: int + message: str + + +class Api(BaseModel): + apiKey: str | None = Field( + None, description='To be used as a dataset parameter value' + ) + apiVersionNumber: str | None = Field( + None, description='To be used as a version parameter value' + ) + apiUrl: AnyUrl | None = Field( + None, description="The URL describing the dataset's fields" + ) + apiDocumentationUrl: AnyUrl | None = Field( + None, description='A URL to the API console for each API' + ) + + +class Apis(RootModel[list[Api]]): + root: list[Api] + + +class Event(BaseModel): + name: str | None = None + + +class Result(BaseModel): + event: Event | None = None diff --git a/tests/data/openapi/default_factory_keep_required_non_nullable.yaml b/tests/data/openapi/default_factory_keep_required_non_nullable.yaml new file mode 100644 index 000000000..6295c23af --- /dev/null +++ b/tests/data/openapi/default_factory_keep_required_non_nullable.yaml @@ -0,0 +1,26 @@ +openapi: 3.1.0 +info: + title: Data test + description: Data test +components: + schemas: + data: + type: object + required: + - number + properties: + number: + description: Number + default: 1 + type: integer + format: int32 + minimum: 0 + block: + type: object + required: + - data + properties: + data: + $ref: '#/components/schemas/data' + dataOptional: + $ref: '#/components/schemas/data' diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py index 65121180d..b0b8a8b0b 100644 --- a/tests/main/openapi/test_main_openapi.py +++ b/tests/main/openapi/test_main_openapi.py @@ -743,7 +743,8 @@ def get_path(path: str) -> str: output_file: Path = tmp_path / "output.py" pyproject_toml_path = Path(DATA_PATH) / "project" / "pyproject.toml" pyproject_toml = ( - pyproject_toml_path.read_text() + pyproject_toml_path + .read_text() .replace("INPUT_PATH", get_path(OPEN_API_DATA_PATH / "api.yaml")) .replace("OUTPUT_PATH", get_path(output_file)) .replace("ALIASES_PATH", get_path(OPEN_API_DATA_PATH / "empty_aliases.json")) @@ -5316,3 +5317,73 @@ def test_main_reuse_model_with_type_alias(output_file: Path) -> None: "--use-type-alias", ], ) + + +@pytest.mark.benchmark +def test_use_default_keep_required_non_nullable(output_file: Path) -> None: + """Test OpenAPI generation with use default option.""" + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "api.yaml", + output_path=output_file, + input_file_type=None, + assert_func=assert_file_content, + extra_args=[ + "--use-default", + "--use-default-keep-required-non-nullable", + ], + ) + + +@pytest.mark.parametrize( + ("output_model", "expected_file"), + [ + ("dataclasses.dataclass", "default_keep_required_non_nullable_dataclass.py"), + ("pydantic_v2.BaseModel", "default_keep_required_non_nullable_pydantic_v2.py"), + ("msgspec.Struct", "default_keep_required_non_nullable_msgspec.py"), + ], +) +@pytest.mark.cli_doc( + options=["--use-default", "--use-default-keep-required-non-nullable"], + option_description="""When using default, keep required field non nullable/non-optionaloptional. + +The `--use-default-keep-required-non-nullable` flag keeps a required field +as non-nullable/optional: +- Dataclasses: `field: int = 1` +- Pydantic: `field: int = 1` +- msgspec: `field: int = 1`""", + input_schema="openapi/default_factory_keep_required_non_nullable.yaml", + cli_args=[ + "--use-default", + "--use-default-keep-required-non-nullable", + ], + related_options=["--use-default"], + model_outputs={ + "dataclass": "main/openapi/default_keep_required_non_nullable_dataclass.py", + "pydantic_v2": "main/openapi/default_keep_required_non_nullable_pydantic_v2.py", + "msgspec": "main/openapi/default_keep_required_non_nullable_msgspec.py", + }, +) +@pytest.mark.benchmark +@LEGACY_BLACK_SKIP +def test_main_use_default_keep_required_non_nullable(output_model: str, expected_file: str, output_file: Path) -> None: + """When using default, keep required field non nullable/non-optionaloptional. + + The `--use-default-keep-required-non-nullable` flag keeps a required field + as non-nullable/optional: + - Dataclasses: `field: int = 1` + - Pydantic: `field: int = 1` + - msgspec: `field: int = 1` + """ + run_main_and_assert( + input_path=OPEN_API_DATA_PATH / "default_factory_keep_required_non_nullable.yaml", + output_path=output_file, + input_file_type="openapi", + assert_func=assert_file_content, + expected_file=expected_file, + extra_args=[ + "--output-model-type", + output_model, + "--use-default", + "--use-default-keep-required-non-nullable", + ], + ) diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index b5511a070..b0ac33d44 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -171,6 +171,7 @@ def _baseline_generate( use_serialization_alias: bool = False, use_frozen_field: bool = False, use_default_factory_for_optional_nested_models: bool = False, + use_default_keep_required_non_nullable: bool = False, formatters: list[Formatter] | None = None, settings_path: Path | None = None, parent_scoped_naming: bool = False, @@ -307,6 +308,7 @@ def __init__( use_serialization_alias: bool = False, use_frozen_field: bool = False, use_default_factory_for_optional_nested_models: bool = False, + use_default_keep_required_non_nullable: bool = False, formatters: list[Formatter] | None = None, defer_formatting: bool = False, parent_scoped_naming: bool = False,