From 6fb16b6185fd3ceab5b7ddaa49c02b481c2cdb3a Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Fri, 2 Jan 2026 17:59:03 +0000 Subject: [PATCH 1/2] Add --use-serialization-alias option for Pydantic v2 --- docs/cli-reference/field-customization.md | 81 +++++++++++++++++++ docs/cli-reference/index.md | 3 +- docs/cli-reference/quick-reference.md | 2 + src/datamodel_code_generator/__main__.py | 2 + .../_types/generate_config_dict.py | 1 + .../_types/parser_config_dicts.py | 1 + src/datamodel_code_generator/arguments.py | 7 ++ src/datamodel_code_generator/cli_options.py | 1 + src/datamodel_code_generator/config.py | 2 + src/datamodel_code_generator/model/base.py | 1 + .../model/pydantic_v2/base_model.py | 3 + src/datamodel_code_generator/parser/base.py | 2 + .../parser/graphql.py | 2 + .../parser/jsonschema.py | 2 + .../parser/openapi.py | 1 + .../expected/main/input_model/config_class.py | 1 + .../main_kr/use_serialization_alias/output.py | 13 +++ .../test_public_api_signature_baseline.py | 2 + tests/test_main_kr.py | 30 +++++++ 19 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 tests/data/expected/main_kr/use_serialization_alias/output.py diff --git a/docs/cli-reference/field-customization.md b/docs/cli-reference/field-customization.md index f4a887798..f56474b89 100644 --- a/docs/cli-reference/field-customization.md +++ b/docs/cli-reference/field-customization.md @@ -26,6 +26,7 @@ | [`--use-field-description-example`](#use-field-description-example) | Add field examples to docstrings. | | [`--use-inline-field-description`](#use-inline-field-description) | Add field descriptions as inline comments. | | [`--use-schema-description`](#use-schema-description) | Use schema description as class docstring. | +| [`--use-serialization-alias`](#use-serialization-alias) | Use serialization_alias instead of alias for field aliasing ... | | [`--use-title-as-name`](#use-title-as-name) | Use schema title as the generated class name. | --- @@ -3663,6 +3664,86 @@ useful for preserving documentation from your schema in the generated code. --- +## `--use-serialization-alias` {#use-serialization-alias} + +Use serialization_alias instead of alias for field aliasing (Pydantic v2 only). + +The `--use-serialization-alias` flag changes field aliasing to use `serialization_alias` +instead of `alias`. This allows setting values using the Pythonic field name while +serializing to the original JSON property name. + +!!! tip "Usage" + + ```bash + datamodel-codegen --input schema.json --use-serialization-alias --output-model-type pydantic_v2.BaseModel # (1)! + ``` + + 1. :material-arrow-left: `--use-serialization-alias` - the option documented here + +??? example "Examples" + + **Input Schema:** + + ```json + { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "first-name": { + "type": "string" + }, + "last-name": { + "type": "string" + }, + "email_address": { + "type": "string" + } + }, + "required": ["first-name", "last-name"] + } + ``` + + **Output:** + + === "With Option" + + ```python + # generated by datamodel-codegen: + # filename: no_alias.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field + + + class Person(BaseModel): + first_name: str = Field(..., serialization_alias='first-name') + last_name: str = Field(..., serialization_alias='last-name') + email_address: str | None = None + ``` + + === "Without Option" + + ```python + # generated by datamodel-codegen: + # filename: no_alias.json + # timestamp: 2019-07-26T00:00:00+00:00 + + from __future__ import annotations + + from pydantic import BaseModel, Field + + + class Person(BaseModel): + first_name: str = Field(..., alias='first-name') + last_name: str = Field(..., alias='last-name') + email_address: str | None = None + ``` + +--- + ## `--use-title-as-name` {#use-title-as-name} Use schema title as the generated class name. diff --git a/docs/cli-reference/index.md b/docs/cli-reference/index.md index 102e6e927..7297a2fdc 100644 --- a/docs/cli-reference/index.md +++ b/docs/cli-reference/index.md @@ -10,7 +10,7 @@ This documentation is auto-generated from test cases. |----------|---------|-------------| | 📁 [Base Options](base-options.md) | 7 | Input/output configuration | | 🔧 [Typing Customization](typing-customization.md) | 27 | Type annotation and import behavior | -| 🏷️ [Field Customization](field-customization.md) | 23 | Field naming and docstring behavior | +| 🏷️ [Field Customization](field-customization.md) | 24 | Field naming and docstring behavior | | 🏗️ [Model Customization](model-customization.md) | 39 | Model generation behavior | | 🎨 [Template Customization](template-customization.md) | 18 | Output formatting and custom rendering | | 📘 [OpenAPI-only Options](openapi-only-options.md) | 7 | OpenAPI-specific features | @@ -203,6 +203,7 @@ This documentation is auto-generated from test cases. - [`--use-pendulum`](typing-customization.md#use-pendulum) - [`--use-root-model-type-alias`](typing-customization.md#use-root-model-type-alias) - [`--use-schema-description`](field-customization.md#use-schema-description) +- [`--use-serialization-alias`](field-customization.md#use-serialization-alias) - [`--use-serialize-as-any`](model-customization.md#use-serialize-as-any) - [`--use-specialized-enum`](typing-customization.md#use-specialized-enum) - [`--use-standard-collections`](typing-customization.md#use-standard-collections) diff --git a/docs/cli-reference/quick-reference.md b/docs/cli-reference/quick-reference.md index 00ad3a6d4..122922a28 100644 --- a/docs/cli-reference/quick-reference.md +++ b/docs/cli-reference/quick-reference.md @@ -82,6 +82,7 @@ datamodel-codegen [OPTIONS] | [`--use-field-description-example`](field-customization.md#use-field-description-example) | Add field examples to docstrings. | | [`--use-inline-field-description`](field-customization.md#use-inline-field-description) | Add field descriptions as inline comments. | | [`--use-schema-description`](field-customization.md#use-schema-description) | Use schema description as class docstring. | +| [`--use-serialization-alias`](field-customization.md#use-serialization-alias) | Use serialization_alias instead of alias for field aliasing (Pydantic v2 only). | | [`--use-title-as-name`](field-customization.md#use-title-as-name) | Use schema title as the generated class name. | ### 🏗️ Model Customization @@ -333,6 +334,7 @@ All options sorted alphabetically: - [`--use-pendulum`](typing-customization.md#use-pendulum) - Use pendulum types for date/time fields instead of datetime ... - [`--use-root-model-type-alias`](typing-customization.md#use-root-model-type-alias) - Generate RootModel as type alias format for better mypy supp... - [`--use-schema-description`](field-customization.md#use-schema-description) - Use schema description as class docstring. +- [`--use-serialization-alias`](field-customization.md#use-serialization-alias) - Use serialization_alias instead of alias for field aliasing ... - [`--use-serialize-as-any`](model-customization.md#use-serialize-as-any) - Wrap fields with subtypes in Pydantic's SerializeAsAny. - [`--use-specialized-enum`](typing-customization.md#use-specialized-enum) - Generate StrEnum/IntEnum for string/integer enums (Python 3.... - [`--use-standard-collections`](typing-customization.md#use-standard-collections) - Use built-in dict/list instead of typing.Dict/List. diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index 88732746e..b66fb7625 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -595,6 +595,7 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) -> frozen_dataclasses: bool = False dataclass_arguments: Optional[DataclassArguments] = None # noqa: UP045 no_alias: bool = False + use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False formatters: list[Formatter] | None = None @@ -1007,6 +1008,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917 keyword_only=config.keyword_only, frozen_dataclasses=config.frozen_dataclasses, no_alias=config.no_alias, + 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, formatters=config.formatters, diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index 7b4e753e3..caed35bdd 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -147,6 +147,7 @@ class GenerateConfigDict(TypedDict): keyword_only: NotRequired[bool] frozen_dataclasses: NotRequired[bool] no_alias: NotRequired[bool] + use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] diff --git a/src/datamodel_code_generator/_types/parser_config_dicts.py b/src/datamodel_code_generator/_types/parser_config_dicts.py index 0520866d8..2983090e3 100644 --- a/src/datamodel_code_generator/_types/parser_config_dicts.py +++ b/src/datamodel_code_generator/_types/parser_config_dicts.py @@ -134,6 +134,7 @@ class ParserConfigDict(TypedDict): keyword_only: NotRequired[bool] frozen_dataclasses: NotRequired[bool] no_alias: NotRequired[bool] + use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py index bf15579ee..07cf31026 100644 --- a/src/datamodel_code_generator/arguments.py +++ b/src/datamodel_code_generator/arguments.py @@ -789,6 +789,13 @@ def start_section(self, heading: str | None) -> None: action="store_true", default=None, ) +field_options.add_argument( + "--use-serialization-alias", + help="Use serialization_alias instead of alias for field aliasing (Pydantic v2 only). " + "This allows setting values using the Pythonic field name while serializing to the original name.", + action="store_true", + default=None, +) field_options.add_argument( "--use-frozen-field", help="Use Field(frozen=True) for readOnly fields (Pydantic v2) or Field(allow_mutation=False) (Pydantic v1)", diff --git a/src/datamodel_code_generator/cli_options.py b/src/datamodel_code_generator/cli_options.py index 499566664..c5fc208f3 100644 --- a/src/datamodel_code_generator/cli_options.py +++ b/src/datamodel_code_generator/cli_options.py @@ -142,6 +142,7 @@ class CLIOptionMeta: "--aliases": CLIOptionMeta(name="--aliases", category=OptionCategory.FIELD), "--default-values": CLIOptionMeta(name="--default-values", category=OptionCategory.FIELD), "--no-alias": CLIOptionMeta(name="--no-alias", category=OptionCategory.FIELD), + "--use-serialization-alias": CLIOptionMeta(name="--use-serialization-alias", category=OptionCategory.FIELD), "--use-title-as-name": CLIOptionMeta(name="--use-title-as-name", category=OptionCategory.FIELD), "--use-schema-description": CLIOptionMeta(name="--use-schema-description", category=OptionCategory.FIELD), "--use-field-description": CLIOptionMeta(name="--use-field-description", category=OptionCategory.FIELD), diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index 20d53aae5..1f3abce75 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -184,6 +184,7 @@ class Config: keyword_only: bool = False frozen_dataclasses: bool = False no_alias: bool = False + use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False formatters: list[Formatter] | None = None @@ -318,6 +319,7 @@ class Config: keyword_only: bool = False frozen_dataclasses: bool = False no_alias: bool = False + use_serialization_alias: bool = False use_frozen_field: bool = False use_default_factory_for_optional_nested_models: bool = False formatters: list[Formatter] | None = None diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index f70c7134f..22f31b4d2 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -191,6 +191,7 @@ class Config: read_only: bool = False write_only: bool = False use_frozen_field: bool = False + use_serialization_alias: bool = False use_default_factory_for_optional_nested_models: bool = False if not TYPE_CHECKING: # pragma: no branch diff --git a/src/datamodel_code_generator/model/pydantic_v2/base_model.py b/src/datamodel_code_generator/model/pydantic_v2/base_model.py index 9e946222b..fd31f2535 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -158,6 +158,9 @@ def _process_data_in_str(self, data: dict[str, Any]) -> None: aliases_repr = ", ".join(repr(a) for a in self.validation_aliases) data["validation_alias"] = _RawRepr(f"AliasChoices({aliases_repr})") + if self.use_serialization_alias and "alias" in data: + data["serialization_alias"] = data.pop("alias") + # **extra is not supported in pydantic 2.0 json_schema_extra = {k: v for k, v in data.items() if k not in self._DEFAULT_FIELD_KEYS} if json_schema_extra: diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index a2c1b67e2..165d98b62 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -938,6 +938,7 @@ def __init__( # noqa: PLR0912, PLR0915 } self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = config.read_only_write_only_model_type self.use_frozen_field: bool = config.use_frozen_field + self.use_serialization_alias: bool = config.use_serialization_alias self.use_default_factory_for_optional_nested_models: bool = ( config.use_default_factory_for_optional_nested_models ) @@ -1463,6 +1464,7 @@ def check_paths( required=True, alias=single_alias, validation_aliases=validation_aliases, + use_serialization_alias=self.use_serialization_alias, ) ) has_imported_literal = any(import_ == IMPORT_LITERAL for import_ in imports) diff --git a/src/datamodel_code_generator/parser/graphql.py b/src/datamodel_code_generator/parser/graphql.py index 347335ae3..be75815ba 100644 --- a/src/datamodel_code_generator/parser/graphql.py +++ b/src/datamodel_code_generator/parser/graphql.py @@ -198,6 +198,7 @@ def _typename_field(self, name: str) -> DataModelFieldBase: use_one_literal_as_default=True, use_default_kwarg=self.use_default_kwarg, has_default=True, + use_serialization_alias=self.use_serialization_alias, ) def _get_default( # noqa: PLR6301 @@ -417,6 +418,7 @@ def parse_field( use_default_kwarg=self.use_default_kwarg, original_name=field_name, has_default=effective_has_default, + use_serialization_alias=self.use_serialization_alias, ) def parse_object_like( diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index 8017248e9..36a9c7d2d 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -1083,6 +1083,7 @@ def get_object_field( # noqa: PLR0913 read_only=self._resolve_field_flag(field, "readOnly"), write_only=self._resolve_field_flag(field, "writeOnly"), 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, ) @@ -2221,6 +2222,7 @@ def _parse_all_of_item( # noqa: PLR0912, PLR0913, PLR0915, PLR0917 alias=single_alias, validation_aliases=validation_aliases, data_type=data_type, + use_serialization_alias=self.use_serialization_alias, ) ) existing_field_names.update({request, field_name}) diff --git a/src/datamodel_code_generator/parser/openapi.py b/src/datamodel_code_generator/parser/openapi.py index 3a711183b..0142b77fc 100644 --- a/src/datamodel_code_generator/parser/openapi.py +++ b/src/datamodel_code_generator/parser/openapi.py @@ -629,6 +629,7 @@ def parse_all_parameters( # noqa: PLR0912, PLR0914 original_name=parameter_name, has_default=effective_has_default, type_has_null=object_schema.type_has_null if object_schema else None, + use_serialization_alias=self.use_serialization_alias, ) ) diff --git a/tests/data/expected/main/input_model/config_class.py b/tests/data/expected/main/input_model/config_class.py index 745ca0693..488295361 100644 --- a/tests/data/expected/main/input_model/config_class.py +++ b/tests/data/expected/main/input_model/config_class.py @@ -222,6 +222,7 @@ class GenerateConfig(TypedDict): keyword_only: NotRequired[bool] frozen_dataclasses: NotRequired[bool] no_alias: NotRequired[bool] + use_serialization_alias: NotRequired[bool] use_frozen_field: NotRequired[bool] use_default_factory_for_optional_nested_models: NotRequired[bool] formatters: NotRequired[list[Formatter] | None] diff --git a/tests/data/expected/main_kr/use_serialization_alias/output.py b/tests/data/expected/main_kr/use_serialization_alias/output.py new file mode 100644 index 000000000..ef840356d --- /dev/null +++ b/tests/data/expected/main_kr/use_serialization_alias/output.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: no_alias.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class Person(BaseModel): + first_name: str = Field(..., serialization_alias='first-name') + last_name: str = Field(..., serialization_alias='last-name') + email_address: str | None = None diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index 22c206204..9482f225b 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -167,6 +167,7 @@ def _baseline_generate( keyword_only: bool = False, frozen_dataclasses: bool = False, no_alias: bool = False, + use_serialization_alias: bool = False, use_frozen_field: bool = False, use_default_factory_for_optional_nested_models: bool = False, formatters: list[Formatter] | None = None, @@ -295,6 +296,7 @@ def __init__( keyword_only: bool = False, frozen_dataclasses: bool = False, no_alias: bool = False, + use_serialization_alias: bool = False, use_frozen_field: bool = False, use_default_factory_for_optional_nested_models: bool = False, formatters: list[Formatter] | None = None, diff --git a/tests/test_main_kr.py b/tests/test_main_kr.py index a97d4c189..d1923ccfa 100644 --- a/tests/test_main_kr.py +++ b/tests/test_main_kr.py @@ -1358,6 +1358,36 @@ def test_no_alias(output_file: Path) -> None: ) +@pytest.mark.cli_doc( + options=["--use-serialization-alias"], + option_description="""Use serialization_alias instead of alias for field aliasing (Pydantic v2 only). + +The `--use-serialization-alias` flag changes field aliasing to use `serialization_alias` +instead of `alias`. This allows setting values using the Pythonic field name while +serializing to the original JSON property name.""", + input_schema="jsonschema/no_alias.json", + cli_args=["--use-serialization-alias", "--output-model-type", "pydantic_v2.BaseModel"], + golden_output="main_kr/use_serialization_alias/output.py", + comparison_output="main_kr/no_alias/without_option.py", +) +@freeze_time("2019-07-26") +def test_use_serialization_alias(output_file: Path) -> None: + """Use serialization_alias instead of alias for field aliasing (Pydantic v2 only). + + The `--use-serialization-alias` flag changes field aliasing to use `serialization_alias` + instead of `alias`. This allows setting values using the Pythonic field name while + serializing to the original JSON property name. + """ + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "no_alias.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file=EXPECTED_MAIN_KR_PATH / "use_serialization_alias" / "output.py", + extra_args=["--use-serialization-alias", "--output-model-type", "pydantic_v2.BaseModel"], + ) + + @pytest.mark.cli_doc( options=["--custom-file-header"], option_description="""Add custom header text to the generated file. From 7fbf864426c05a2c84a946a71325f22c982f0ef7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 2 Jan 2026 18:04:14 +0000 Subject: [PATCH 2/2] docs: update CLI reference documentation and prompt data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated by GitHub Actions --- src/datamodel_code_generator/prompt_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/datamodel_code_generator/prompt_data.py b/src/datamodel_code_generator/prompt_data.py index 61011973f..d63375ce3 100644 --- a/src/datamodel_code_generator/prompt_data.py +++ b/src/datamodel_code_generator/prompt_data.py @@ -129,6 +129,7 @@ "--use-pendulum": "Use pendulum types for date/time fields instead of datetime module.", "--use-root-model-type-alias": "Generate RootModel as type alias format for better mypy support.", "--use-schema-description": "Use schema description as class docstring.", + "--use-serialization-alias": "Use serialization_alias instead of alias for field aliasing (Pydantic v2 only).", "--use-serialize-as-any": "Wrap fields with subtypes in Pydantic's SerializeAsAny.", "--use-specialized-enum": "Generate StrEnum/IntEnum for string/integer enums (Python 3.11+).", "--use-standard-collections": "Use built-in dict/list instead of typing.Dict/List.",