Skip to content
Merged
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) | 19 | Type annotation and import behavior |
| 🏷️ [Field Customization](field-customization.md) | 21 | Field naming and docstring behavior |
| 🏗️ [Model Customization](model-customization.md) | 29 | Model generation behavior |
| 🏗️ [Model Customization](model-customization.md) | 30 | 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 @@ -152,6 +152,7 @@ This documentation is auto-generated from test cases.

### T {#t}

- [`--target-pydantic-version`](model-customization.md#target-pydantic-version)
- [`--target-python-version`](model-customization.md#target-python-version)
- [`--type-mappings`](typing-customization.md#type-mappings)

Expand Down
82 changes: 82 additions & 0 deletions docs/cli-reference/model-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
| [`--skip-root-model`](#skip-root-model) | Skip generation of root model when schema contains nested de... |
| [`--strict-nullable`](#strict-nullable) | Treat default field as a non-nullable field. |
| [`--strip-default-none`](#strip-default-none) | Remove fields with None as default value from generated mode... |
| [`--target-pydantic-version`](#target-pydantic-version) | Target Pydantic version for generated code compatibility. |
| [`--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. |
Expand Down Expand Up @@ -4647,6 +4648,87 @@ default to None.

---

## `--target-pydantic-version` {#target-pydantic-version}

Target Pydantic version for generated code compatibility.

The `--target-pydantic-version` flag controls Pydantic version-specific config:

- **2**: Uses `populate_by_name=True` (compatible with Pydantic 2.0-2.10)
- **2.11**: Uses `validate_by_name=True` (for Pydantic 2.11+)

This prevents breaking changes when generated code is used on older Pydantic versions.

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --target-pydantic-version 2.11 --allow-population-by-field-name --output-model-type pydantic_v2.BaseModel # (1)!
```

1. :material-arrow-left: `--target-pydantic-version` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": ["string", "null"],
"description": "The person's last name."
},
"age": {
"description": "Age in years which must be equal to or greater than zero.",
"type": "integer",
"minimum": 0
},
"friends": {
"type": "array"
},
"comment": {
"type": "null"
}
}
}
```

**Output:**

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

from __future__ import annotations

from typing import Any

from pydantic import BaseModel, ConfigDict, Field, conint


class Person(BaseModel):
model_config = ConfigDict(
validate_by_name=True,
)
firstName: str | None = Field(None, description="The person's first name.")
lastName: str | None = Field(None, description="The person's last name.")
age: conint(ge=0) | None = Field(
None, description='Age in years which must be equal to or greater than zero.'
)
friends: list[Any] | None = None
comment: None = None
```

---

## `--target-python-version` {#target-python-version}

Target Python version for generated code syntax and imports.
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 @@ -95,6 +95,7 @@ datamodel-codegen [OPTIONS]
| [`--skip-root-model`](model-customization.md#skip-root-model) | Skip generation of root model when schema contains nested definitions. |
| [`--strict-nullable`](model-customization.md#strict-nullable) | Treat default field as a non-nullable field. |
| [`--strip-default-none`](model-customization.md#strip-default-none) | Remove fields with None as default value from generated models. |
| [`--target-pydantic-version`](model-customization.md#target-pydantic-version) | Target Pydantic version for generated code compatibility. |
| [`--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. |
Expand Down Expand Up @@ -253,6 +254,7 @@ All options sorted alphabetically:
- [`--strict-nullable`](model-customization.md#strict-nullable) - Treat default field as a non-nullable field.
- [`--strict-types`](typing-customization.md#strict-types) - Enable strict type validation for specified Python types.
- [`--strip-default-none`](model-customization.md#strip-default-none) - Remove fields with None as default value from generated mode...
- [`--target-pydantic-version`](model-customization.md#target-pydantic-version) - Target Pydantic version for generated code compatibility.
- [`--target-python-version`](model-customization.md#target-python-version) - Target Python version for generated code syntax and imports.
- [`--type-mappings`](typing-customization.md#type-mappings) - Override default type mappings for schema formats.
- [`--union-mode`](model-customization.md#union-mode) - Union mode for combining anyOf/oneOf schemas (smart or left_...
Expand Down
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -211,10 +211,6 @@ skip = '.git,*.lock,tests,docs/cli-reference,CHANGELOG.md,docs/changelog.md'
[tool.pytest.ini_options]
filterwarnings = [
"error",
"ignore:^.*The `parse_obj` method is deprecated; use `model_validate` instead.*",
"ignore:^.*The `__fields_set__` attribute is deprecated, use `model_fields_set` instead.*",
"ignore:^.*The `dict` method is deprecated; use `model_dump` instead.*",
"ignore:^.*The `copy` method is deprecated; use `model_copy` instead.*",
"ignore:^.*`--validation` option is deprecated.*",
"ignore:^.*Field name `name` is duplicated on Pet.*",
"ignore:^.*format of 'unknown-type' not understood for 'string' - using default.*",
Expand Down
14 changes: 14 additions & 0 deletions src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,17 @@ class ModuleSplitMode(Enum):
Single = "single"


class TargetPydanticVersion(Enum):
"""Target Pydantic version for generated code.

V2: Generate code compatible with Pydantic 2.0+ (uses populate_by_name).
V2_11: Generate code for Pydantic 2.11+ (uses validate_by_name).
"""

V2 = "2"
V2_11 = "2.11"


class Error(Exception):
"""Base exception for datamodel-code-generator errors."""

Expand Down Expand Up @@ -400,6 +411,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
output: Path | None = None,
output_model_type: DataModelType = DataModelType.PydanticBaseModel,
target_python_version: PythonVersion = PythonVersionMin,
target_pydantic_version: TargetPydanticVersion | None = None,
base_class: str = "",
additional_imports: list[str] | None = None,
custom_template_dir: Path | None = None,
Expand Down Expand Up @@ -749,6 +761,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
type_mappings=type_mappings,
read_only_write_only_model_type=read_only_write_only_model_type,
field_type_collision_strategy=field_type_collision_strategy,
target_pydantic_version=target_pydantic_version,
**kwargs,
)

Expand Down Expand Up @@ -902,5 +915,6 @@ def infer_input_type(text: str) -> InputFileType:
"ModuleSplitMode",
"PythonVersion",
"ReadOnlyWriteOnlyModelType",
"TargetPydanticVersion",
"generate",
]
3 changes: 3 additions & 0 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
OpenAPIScope,
ReadOnlyWriteOnlyModelType,
ReuseScope,
TargetPydanticVersion,
enable_debug_message,
generate,
)
Expand Down Expand Up @@ -378,6 +379,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
debug: bool = False
disable_warnings: bool = False
target_python_version: PythonVersion = PythonVersionMin
target_pydantic_version: Optional[TargetPydanticVersion] = None # noqa: UP045
base_class: str = ""
additional_imports: Optional[list[str]] = None # noqa: UP045
custom_template_dir: Optional[Path] = None # noqa: UP045
Expand Down Expand Up @@ -687,6 +689,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
output=output,
output_model_type=config.output_model_type,
target_python_version=config.target_python_version,
target_pydantic_version=config.target_pydantic_version,
base_class=config.base_class,
additional_imports=config.additional_imports,
custom_template_dir=config.custom_template_dir,
Expand Down
9 changes: 9 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
OpenAPIScope,
ReadOnlyWriteOnlyModelType,
ReuseScope,
TargetPydanticVersion,
)
from datamodel_code_generator.format import DateClassType, DatetimeClassType, Formatter, PythonVersion
from datamodel_code_generator.model.pydantic_v2 import UnionMode
Expand Down Expand Up @@ -271,6 +272,14 @@ def start_section(self, heading: str | None) -> None:
help="target python version",
choices=[v.value for v in PythonVersion],
)
model_options.add_argument(
"--target-pydantic-version",
help="Target Pydantic version for generated code. "
"'2': Pydantic 2.0+ compatible (default, uses populate_by_name). "
"'2.11': Pydantic 2.11+ (uses validate_by_name).",
choices=[v.value for v in TargetPydanticVersion],
default=None,
)
model_options.add_argument(
"--treat-dot-as-module",
help="Treat dotted schema names as module paths, creating nested directory structures (e.g., 'foo.bar.Model' "
Expand Down
1 change: 1 addition & 0 deletions src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class CLIOptionMeta:
# ==========================================================================
"--output-model-type": CLIOptionMeta(name="--output-model-type", category=OptionCategory.MODEL),
"--target-python-version": CLIOptionMeta(name="--target-python-version", category=OptionCategory.MODEL),
"--target-pydantic-version": CLIOptionMeta(name="--target-pydantic-version", category=OptionCategory.MODEL),
"--base-class": CLIOptionMeta(name="--base-class", category=OptionCategory.MODEL),
"--class-name": CLIOptionMeta(name="--class-name", category=OptionCategory.MODEL),
"--frozen-dataclasses": CLIOptionMeta(name="--frozen-dataclasses", category=OptionCategory.MODEL),
Expand Down
27 changes: 15 additions & 12 deletions src/datamodel_code_generator/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
chain_as_tuple,
get_optional_type,
)
from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict
from datamodel_code_generator.util import PYDANTIC_V2, ConfigDict, model_copy, model_dump, model_validate

__all__ = ["WrappedDefault"]

Expand Down Expand Up @@ -90,31 +90,34 @@ class Config:
@cached_property
def has_constraints(self) -> bool:
"""Check if any constraint values are set."""
return any(v is not None for v in self.dict().values())
return any(v is not None for v in model_dump(self).values())

@staticmethod
def merge_constraints(a: ConstraintsBaseT | None, b: ConstraintsBaseT | None) -> ConstraintsBaseT | None:
"""Merge two constraint objects, with b taking precedence over a."""
constraints_class = None
if isinstance(a, ConstraintsBase): # pragma: no cover
root_type_field_constraints = {k: v for k, v in a.dict(by_alias=True).items() if v is not None}
root_type_field_constraints = {k: v for k, v in model_dump(a, by_alias=True).items() if v is not None}
constraints_class = a.__class__
else:
root_type_field_constraints = {} # pragma: no cover

if isinstance(b, ConstraintsBase): # pragma: no cover
model_field_constraints = {k: v for k, v in b.dict(by_alias=True).items() if v is not None}
model_field_constraints = {k: v for k, v in model_dump(b, by_alias=True).items() if v is not None}
constraints_class = constraints_class or b.__class__
else:
model_field_constraints = {}

if constraints_class is None or not issubclass(constraints_class, ConstraintsBase): # pragma: no cover
return None

return constraints_class.parse_obj({
**root_type_field_constraints,
**model_field_constraints,
})
return model_validate(
constraints_class,
{
**root_type_field_constraints,
**model_field_constraints,
},
)


class DataModelFieldBase(_BaseModel):
Expand Down Expand Up @@ -373,14 +376,14 @@ def fall_back_to_nullable(self) -> bool:

def copy_deep(self) -> Self:
"""Create a deep copy of this field to avoid mutating the original."""
copied = self.copy()
copied = model_copy(self)
copied.parent = None
copied.extras = deepcopy(self.extras)
copied.data_type = self.data_type.copy()
copied.data_type = model_copy(self.data_type)
if self.data_type.data_types:
copied.data_type.data_types = [dt.copy() for dt in self.data_type.data_types]
copied.data_type.data_types = [model_copy(dt) for dt in self.data_type.data_types]
if self.data_type.dict_key:
copied.data_type.dict_key = self.data_type.dict_key.copy()
copied.data_type.dict_key = model_copy(self.data_type.dict_key)
return copied

def replace_data_type(self, new_data_type: DataType, *, clear_old_parent: bool = True) -> None:
Expand Down
3 changes: 2 additions & 1 deletion src/datamodel_code_generator/model/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
_remove_none_from_union,
chain_as_tuple,
)
from datamodel_code_generator.util import model_dump

UNSET_TYPE = "UnsetType"

Expand Down Expand Up @@ -389,7 +390,7 @@ def _get_meta_string(self) -> str | None:
**data,
**{
k: self._get_strict_field_constraint_value(k, v)
for k, v in self.constraints.dict().items()
for k, v in model_dump(self.constraints).items()
if k in self._META_FIELD_KEYS
},
}
Expand Down
12 changes: 11 additions & 1 deletion src/datamodel_code_generator/model/pydantic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Any, Optional

from pydantic import BaseModel as _BaseModel

Expand Down Expand Up @@ -36,6 +36,16 @@ class Config(_BaseModel):
orm_mode: Optional[bool] = None # noqa: UP045
validate_assignment: Optional[bool] = None # noqa: UP045

def dict( # type: ignore[override]
self, **kwargs: Any
) -> dict[str, Any]:
"""Version-compatible dict method for templates."""
from datamodel_code_generator.util import PYDANTIC_V2 # noqa: PLC0415

if PYDANTIC_V2:
return self.model_dump(**kwargs)
return super().dict(**kwargs)


__all__ = [
"BaseModel",
Expand Down
5 changes: 3 additions & 2 deletions src/datamodel_code_generator/model/pydantic/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
IMPORT_FIELD,
)
from datamodel_code_generator.types import STANDARD_LIST, UnionIntFloat, chain_as_tuple
from datamodel_code_generator.util import model_dump, model_validate

if TYPE_CHECKING:
from collections import defaultdict
Expand Down Expand Up @@ -199,7 +200,7 @@ def __str__(self) -> str: # noqa: PLR0912
if any(d.import_ == IMPORT_ANYURL for d in self.data_type.all_data_types)
else {
k: self._get_strict_field_constraint_value(k, v)
for k, v in self.constraints.dict(exclude_unset=True).items()
for k, v in model_dump(self.constraints, exclude_unset=True).items()
}
),
}
Expand Down Expand Up @@ -402,4 +403,4 @@ def __init__( # noqa: PLR0912, PLR0913
if config_parameters:
from datamodel_code_generator.model.pydantic import Config # noqa: PLC0415

self.extra_template_data["config"] = Config.parse_obj(config_parameters) # pyright: ignore[reportArgumentType]
self.extra_template_data["config"] = model_validate(Config, config_parameters) # pyright: ignore[reportArgumentType]
Loading
Loading