Skip to content

Commit bff6a30

Browse files
butvinmclaude
andauthored
Fix --allow-population-by-field-name for pydantic v2 dataclass output (#3013)
* Fix --allow-population-by-field-name for pydantic v2 dataclass output (#3010) DataClass.__init__ never read allow_population_by_field_name from extra_template_data, so the flag was silently ignored for pydantic_v2.dataclass output. Add _CONFIG_ATTRIBUTES processing (matching the BaseModel pattern) to emit populate_by_name in ConfigDict, with validate_by_name support for --target-pydantic-version 2.11. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add test for V2.11 validate_by_name path to fix coverage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c36ac6c commit bff6a30

4 files changed

Lines changed: 87 additions & 3 deletions

File tree

src/datamodel_code_generator/model/pydantic_v2/dataclass.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from datamodel_code_generator.model import DataModel, DataModelFieldBase
1111
from datamodel_code_generator.model.base import UNDEFINED
1212
from datamodel_code_generator.model.dataclass import has_field_assignment
13-
from datamodel_code_generator.model.pydantic_v2.base_model import Constraints
13+
from datamodel_code_generator.model.pydantic_v2.base_model import ConfigAttribute, Constraints
1414
from datamodel_code_generator.model.pydantic_v2.base_model import (
1515
DataModelField as DataModelFieldV2,
1616
)
@@ -35,6 +35,17 @@ class DataClass(DataModel):
3535
DEFAULT_IMPORTS: ClassVar[tuple[Import, ...]] = (IMPORT_PYDANTIC_DATACLASS,)
3636
SUPPORTS_DISCRIMINATOR: ClassVar[bool] = True
3737
SUPPORTS_KW_ONLY: ClassVar[bool] = True
38+
# frozen/allow_mutation are handled as dataclass decorator arguments, not ConfigDict
39+
_CONFIG_ATTRIBUTES_V2: ClassVar[list[ConfigAttribute]] = [
40+
ConfigAttribute("allow_population_by_field_name", "populate_by_name", False), # noqa: FBT003
41+
ConfigAttribute("populate_by_name", "populate_by_name", False), # noqa: FBT003
42+
ConfigAttribute("use_attribute_docstrings", "use_attribute_docstrings", False), # noqa: FBT003
43+
]
44+
_CONFIG_ATTRIBUTES_V2_11: ClassVar[list[ConfigAttribute]] = [
45+
ConfigAttribute("allow_population_by_field_name", "validate_by_name", False), # noqa: FBT003
46+
ConfigAttribute("populate_by_name", "validate_by_name", False), # noqa: FBT003
47+
ConfigAttribute("use_attribute_docstrings", "use_attribute_docstrings", False), # noqa: FBT003
48+
]
3849

3950
def __init__( # noqa: PLR0913
4051
self,
@@ -90,8 +101,12 @@ def __init__( # noqa: PLR0913
90101
if extra:
91102
config_parameters["extra"] = extra
92103

93-
if self.extra_template_data.get("use_attribute_docstrings"):
94-
config_parameters["use_attribute_docstrings"] = True
104+
config_attributes = self._get_config_attributes()
105+
for from_, to, invert in config_attributes:
106+
if from_ in self.extra_template_data:
107+
config_parameters[to] = (
108+
not self.extra_template_data[from_] if invert else self.extra_template_data[from_]
109+
)
95110

96111
for data_type in self.all_data_types:
97112
if data_type.is_custom_type: # pragma: no cover
@@ -102,6 +117,15 @@ def __init__( # noqa: PLR0913
102117
self._additional_imports.append(IMPORT_CONFIG_DICT)
103118
self.extra_template_data["config"] = config_parameters
104119

120+
def _get_config_attributes(self) -> list[ConfigAttribute]:
121+
"""Get config attributes based on target Pydantic version."""
122+
from datamodel_code_generator import TargetPydanticVersion # noqa: PLC0415
123+
124+
target_version = self.extra_template_data.get("target_pydantic_version")
125+
if target_version == TargetPydanticVersion.V2_11:
126+
return self._CONFIG_ATTRIBUTES_V2_11
127+
return self._CONFIG_ATTRIBUTES_V2
128+
105129
def _get_config_extra(self) -> str | None:
106130
"""Get extra field configuration for ConfigDict."""
107131
additional_properties = self.extra_template_data.get("additionalProperties")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: simple_string.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import ConfigDict
8+
from pydantic.dataclasses import dataclass
9+
10+
11+
@dataclass(config=ConfigDict(populate_by_name=True))
12+
class Model:
13+
s: str
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# generated by datamodel-codegen:
2+
# filename: simple_string.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from pydantic import ConfigDict
8+
from pydantic.dataclasses import dataclass
9+
10+
11+
@dataclass(config=ConfigDict(validate_by_name=True))
12+
class Model:
13+
s: str

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
InputFileType,
1919
PythonVersion,
2020
PythonVersionMin,
21+
TargetPydanticVersion,
2122
chdir,
2223
generate,
2324
)
@@ -1844,6 +1845,39 @@ def test_main_generate_pydantic_v2_dataclass_use_attribute_docstrings(tmp_path:
18441845
assert_file_content(output_file, "pydantic_v2_dataclass_use_attribute_docstrings.py")
18451846

18461847

1848+
def test_main_generate_pydantic_v2_dataclass_allow_population_by_field_name(tmp_path: Path) -> None:
1849+
"""Test pydantic_v2.dataclass with allow_population_by_field_name."""
1850+
output_file: Path = tmp_path / "output.py"
1851+
input_ = (JSON_SCHEMA_DATA_PATH / "simple_string.json").relative_to(Path.cwd())
1852+
assert not input_.is_absolute()
1853+
generate(
1854+
input_=input_,
1855+
input_file_type=InputFileType.JsonSchema,
1856+
output=output_file,
1857+
output_model_type=DataModelType.PydanticV2Dataclass,
1858+
allow_population_by_field_name=True,
1859+
)
1860+
1861+
assert_file_content(output_file, "pydantic_v2_dataclass_populate_by_name.py")
1862+
1863+
1864+
def test_main_generate_pydantic_v2_dataclass_allow_population_by_field_name_v2_11(tmp_path: Path) -> None:
1865+
"""Test pydantic_v2.dataclass with allow_population_by_field_name and target v2.11."""
1866+
output_file: Path = tmp_path / "output.py"
1867+
input_ = (JSON_SCHEMA_DATA_PATH / "simple_string.json").relative_to(Path.cwd())
1868+
assert not input_.is_absolute()
1869+
generate(
1870+
input_=input_,
1871+
input_file_type=InputFileType.JsonSchema,
1872+
output=output_file,
1873+
output_model_type=DataModelType.PydanticV2Dataclass,
1874+
allow_population_by_field_name=True,
1875+
target_pydantic_version=TargetPydanticVersion.V2_11,
1876+
)
1877+
1878+
assert_file_content(output_file, "pydantic_v2_dataclass_validate_by_name.py")
1879+
1880+
18471881
def test_main_generate_pydantic_v2_dataclass_extra_allow(tmp_path: Path) -> None:
18481882
"""Test pydantic_v2.dataclass with extra='allow'."""
18491883
output_file: Path = tmp_path / "output.py"

0 commit comments

Comments
 (0)