Skip to content
Closed
6 changes: 5 additions & 1 deletion docs/cli-reference/model-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -1368,11 +1368,15 @@ model types (Pydantic v1/v2, dataclass, TypedDict, msgspec) handle const fields.

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field


class Api(BaseModel):
version: str = Field('v1', const=True, description='The version of this API')
version: Literal['v1'] = Field(
'v1', const=True, description='The version of this API'
)
```

=== "Pydantic v2"
Expand Down
12 changes: 9 additions & 3 deletions docs/cli-reference/template-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -2553,11 +2553,15 @@ helps maintain consistency with codebases that prefer double-quote formatting.

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field, confloat


class MapState1(BaseModel):
map_view_mode: str = Field("MODE_2D", alias="mapViewMode", const=True)
map_view_mode: Literal["MODE_2D"] = Field(
"MODE_2D", alias="mapViewMode", const=True
)


class MapState2(BaseModel):
Expand All @@ -2567,8 +2571,10 @@ helps maintain consistency with codebases that prefer double-quote formatting.
bearing: Bearing | None = None
pitch: Pitch
drag_rotate: DragRotate | None = Field(None, alias="dragRotate")
map_split_mode: str = Field("SWIPE_COMPARE", alias="mapSplitMode", const=True)
is_split: bool = Field(True, alias="isSplit", const=True)
map_split_mode: Literal["SWIPE_COMPARE"] = Field(
"SWIPE_COMPARE", alias="mapSplitMode", const=True
)
is_split: Literal[True] = Field(True, alias="isSplit", const=True)


class MapState3(BaseModel):
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
field_constraints: bool = False,
snake_case_field: bool = False,
strip_default_none: bool = False,
aliases: Mapping[str, str] | None = None,
aliases: Mapping[str, str | list[str]] | None = None,
disable_timestamp: bool = False,
enable_version_header: bool = False,
enable_command_header: bool = False,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GenerateConfigDict(TypedDict):
field_constraints: NotRequired[bool]
snake_case_field: NotRequired[bool]
strip_default_none: NotRequired[bool]
aliases: NotRequired[Mapping[str, str] | None]
aliases: NotRequired[Mapping[str, str | list[str]] | None]
disable_timestamp: NotRequired[bool]
enable_version_header: NotRequired[bool]
enable_command_header: NotRequired[bool]
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/_types/parser_config_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ParserConfigDict(TypedDict):
field_constraints: NotRequired[bool]
snake_case_field: NotRequired[bool]
strip_default_none: NotRequired[bool]
aliases: NotRequired[Mapping[str, str] | None]
aliases: NotRequired[Mapping[str, str | list[str]] | None]
allow_population_by_field_name: NotRequired[bool]
apply_default_values_for_required_fields: NotRequired[bool]
allow_extra_fields: NotRequired[bool]
Expand Down
49 changes: 46 additions & 3 deletions src/datamodel_code_generator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import TYPE_CHECKING, Annotated, Any

from pydantic import BaseModel, Field
from typing_extensions import Self

from datamodel_code_generator.enums import (
DEFAULT_SHARED_MODULE_NAME,
Expand Down Expand Up @@ -85,7 +86,7 @@ class Config:
field_constraints: bool = False
snake_case_field: bool = False
strip_default_none: bool = False
aliases: Mapping[str, str] | None = None
aliases: Mapping[str, str | list[str]] | None = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find all usages of the aliases field and its access patterns
rg -nP --type=py 'aliases' -C3 | head -200

Repository: koxudaxi/datamodel-code-generator

Length of output: 17256


🏁 Script executed:

# Check the watch.py function signature and how it uses aliases
cat -n src/datamodel_code_generator/watch.py | head -60

Repository: koxudaxi/datamodel-code-generator

Length of output: 2632


🏁 Script executed:

# Check the generate function signature that watch.py calls
rg -n 'def generate' src/datamodel_code_generator/__init__.py -A 20 | head -80

Repository: koxudaxi/datamodel-code-generator

Length of output: 1141


Fix type signature mismatch in watch.py for aliases parameter.

The watch_and_regenerate function declares aliases: dict[str, str] | None (line 26) but should be dict[str, str | list[str]] | None to match the updated signature in generate() and the configuration classes. The type must support both str and list[str] values to maintain consistency with the extended type in GenerateConfig and ParserConfig.

🤖 Prompt for AI Agents
In src/datamodel_code_generator/config.py around line 89 and in
src/datamodel_code_generator/watch.py around line 26, the type for aliases must
allow both string and list[string]; update the watch.py parameter and any
related annotations from dict[str, str] | None to dict[str, str | list[str]] |
None (or the equivalent Mapping/typing form used across the project) so it
matches GenerateConfig/ParserConfig; ensure any usages or type checks that
assume a simple str are adjusted to handle list[str] as well and update imports
(typing.Mapping or list union) if required.

disable_timestamp: bool = False
enable_version_header: bool = False
enable_command_header: bool = False
Expand Down Expand Up @@ -201,9 +202,36 @@ class ParserConfig(BaseModel):
class Config:
"""Pydantic v1 model config."""

extra = "forbid"
extra = "allow"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Inconsistent extra field handling between Pydantic v1 and v2.

Pydantic v2 config (line 199) has extra="forbid", but this change sets extra="allow" for v1 (line 205). This creates version-dependent validation behavior where:

  • v1 silently accepts extra/invalid fields
  • v2 rejects them with a validation error

This inconsistency undermines cross-version compatibility and could mask typos or incorrect field names when running on v1.

🔎 Proposed fix
-            extra = "allow"
+            extra = "forbid"

If extra fields are needed for the config-merging pattern described in the PR, consider handling them explicitly in from_options rather than changing the model's validation behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
extra = "allow"
extra = "forbid"
🤖 Prompt for AI Agents
In src/datamodel_code_generator/config.py around line 205, the change sets
Pydantic v1 model config extra="allow" which conflicts with the v2 config
extra="forbid" at ~line 199 and creates version-dependent behavior; revert the
v1 config to extra="forbid" to match v2, and if the codebase needs to accept or
merge extra fields implement explicit handling inside the classmethod
from_options (collect/populate allowed extras there) rather than changing the
model's validation behavior so both Pydantic versions reject unknown fields by
default.

arbitrary_types_allowed = True

@classmethod
def from_options(cls, options: dict[str, Any]) -> Self:
"""Create a ParserConfig from options dict with version-compatible validation."""
if is_pydantic_v2():
from datamodel_code_generator.model.base import ( # noqa: PLC0415
DataModel,
DataModelFieldBase,
)
from datamodel_code_generator.types import DataTypeManager, StrictTypes # noqa: PLC0415
Comment on lines +212 to +216
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused noqa directives.

The noqa: PLC0415 directives on lines 212 and 216 are not needed, as indicated by static analysis.

🔎 Proposed fix
-            from datamodel_code_generator.model.base import (  # noqa: PLC0415
+            from datamodel_code_generator.model.base import (
                 DataModel,
                 DataModelFieldBase,
             )
-            from datamodel_code_generator.types import DataTypeManager, StrictTypes  # noqa: PLC0415
+            from datamodel_code_generator.types import DataTypeManager, StrictTypes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
from datamodel_code_generator.model.base import ( # noqa: PLC0415
DataModel,
DataModelFieldBase,
)
from datamodel_code_generator.types import DataTypeManager, StrictTypes # noqa: PLC0415
from datamodel_code_generator.model.base import (
DataModel,
DataModelFieldBase,
)
from datamodel_code_generator.types import DataTypeManager, StrictTypes
🧰 Tools
🪛 Ruff (0.14.10)

212-212: Unused noqa directive (non-enabled: PLC0415)

Remove unused noqa directive

(RUF100)


216-216: Unused noqa directive (non-enabled: PLC0415)

Remove unused noqa directive

(RUF100)

🤖 Prompt for AI Agents
In src/datamodel_code_generator/config.py around lines 212 to 216, the trailing
"noqa: PLC0415" directives on the two import lines are unnecessary; remove the "
# noqa: PLC0415" suffixes from the import statements so the imports remain the
same but without the unused noqa directives, keeping the lines otherwise
unchanged.


cls.model_rebuild(
_types_namespace={
"DataModel": DataModel,
"DataModelFieldBase": DataModelFieldBase,
"DataTypeManager": DataTypeManager,
"StrictTypes": StrictTypes,
}
)
return cls.model_validate(options)
# For Pydantic v1, use construct() to skip validation (forward refs not resolved)
defaults: dict[str, Any] = {}
for field_name, field in cls.__fields__.items(): # type: ignore[attr-defined]
# Note: ParserConfig fields don't use default_factory, so we only handle static defaults
defaults[field_name] = field.default
defaults.update(options)
return cls.construct(**defaults) # type: ignore[return-value]
Comment on lines +227 to +233
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pydantic v1 path bypasses validation entirely using construct().

The Pydantic v1 code path uses construct() to skip validation due to unresolved forward references. However, this bypasses ALL validation, not just forward-ref resolution, which could allow invalid or inconsistent configuration objects to be created.

Consider alternative approaches:

  • Use parse_obj() or model_validate() equivalents that perform validation
  • Pre-resolve the required types before validation (similar to the v2 approach)
  • Document why validation must be skipped and the risks involved
🤖 Prompt for AI Agents
In src/datamodel_code_generator/config.py around lines 227 to 233, the Pydantic
v1 path currently uses cls.construct(...) which skips all validation; instead,
call cls.update_forward_refs() to resolve forward references and then
instantiate with validation (e.g., use cls.parse_obj(defaults) or
cls(**defaults)) so validation runs; also catch and re-raise or log
pydantic.ValidationError with a clear message to preserve existing error
behavior.


data_model_type: type[DataModel] = pydantic_model.BaseModel
data_model_root_type: type[DataModel] = pydantic_model.CustomRootType
data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager
Expand All @@ -220,7 +248,7 @@ class Config:
field_constraints: bool = False
snake_case_field: bool = False
strip_default_none: bool = False
aliases: Mapping[str, str] | None = None
aliases: Mapping[str, str | list[str]] | None = None
allow_population_by_field_name: bool = False
apply_default_values_for_required_fields: bool = False
allow_extra_fields: bool = False
Expand Down Expand Up @@ -316,6 +344,21 @@ class Config:
target_pydantic_version: TargetPydanticVersion | None = None


class OpenAPIParserConfig(ParserConfig):
"""Configuration model for OpenAPIParser.__init__()."""

openapi_scopes: list[OpenAPIScope] | None = None
include_path_parameters: bool = False
use_status_code_in_response_name: bool = False


class GraphQLParserConfig(ParserConfig):
"""Configuration model for GraphQLParser.__init__()."""

data_model_scalar_type: type[DataModel] | None = None
data_model_union_type: type[DataModel] | None = None


class ParseConfig(BaseModel):
"""Configuration model for Parser.parse()."""

Expand Down
Loading
Loading