Skip to content

Commit 8bbc4df

Browse files
authored
Add --use-frozen-field option for JSON Schema readOnly support (#2650)
* Add --use-frozen-field option to support frozen fields in Pydantic models * Add use_frozen_field parameter to OpenAPI parser initialization
1 parent f639855 commit 8bbc4df

File tree

19 files changed

+211
-1
lines changed

19 files changed

+211
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,8 @@ Field customization:
444444
default values.
445445
--use-field-description
446446
Use schema description to populate field docstring
447+
--use-frozen-field Use Field(frozen=True) for readOnly fields (Pydantic v2) or
448+
Field(allow_mutation=False) (Pydantic v1)
447449
--use-inline-field-description
448450
Use schema description to populate field docstring as inline
449451
docstring

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,8 @@ Field customization:
436436
default values.
437437
--use-field-description
438438
Use schema description to populate field docstring
439+
--use-frozen-field Use Field(frozen=True) for readOnly fields (Pydantic v2) or
440+
Field(allow_mutation=False) (Pydantic v1)
439441
--use-inline-field-description
440442
Use schema description to populate field docstring as inline
441443
docstring

src/datamodel_code_generator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
443443
keyword_only: bool = False,
444444
frozen_dataclasses: bool = False,
445445
no_alias: bool = False,
446+
use_frozen_field: bool = False,
446447
formatters: list[Formatter] = DEFAULT_FORMATTERS,
447448
settings_path: Path | None = None,
448449
parent_scoped_naming: bool = False,
@@ -682,6 +683,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
682683
keyword_only=keyword_only,
683684
frozen_dataclasses=frozen_dataclasses,
684685
no_alias=no_alias,
686+
use_frozen_field=use_frozen_field,
685687
formatters=formatters,
686688
encoding=encoding,
687689
parent_scoped_naming=parent_scoped_naming,

src/datamodel_code_generator/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
449449
frozen_dataclasses: bool = False
450450
dataclass_arguments: Optional[DataclassArguments] = None # noqa: UP045
451451
no_alias: bool = False
452+
use_frozen_field: bool = False
452453
formatters: list[Formatter] = DEFAULT_FORMATTERS
453454
parent_scoped_naming: bool = False
454455
disable_future_imports: bool = False
@@ -879,6 +880,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
879880
keyword_only=config.keyword_only,
880881
frozen_dataclasses=config.frozen_dataclasses,
881882
no_alias=config.no_alias,
883+
use_frozen_field=config.use_frozen_field,
882884
formatters=config.formatters,
883885
settings_path=config.output if config.check else None,
884886
parent_scoped_naming=config.parent_scoped_naming,

src/datamodel_code_generator/arguments.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,12 @@ def start_section(self, heading: str | None) -> None:
548548
action="store_true",
549549
default=None,
550550
)
551+
field_options.add_argument(
552+
"--use-frozen-field",
553+
help="Use Field(frozen=True) for readOnly fields (Pydantic v2) or Field(allow_mutation=False) (Pydantic v1)",
554+
action="store_true",
555+
default=None,
556+
)
551557

552558
# ======================================================================================
553559
# Options for templating output

src/datamodel_code_generator/model/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ class Config:
151151
type_has_null: Optional[bool] = None # noqa: UP045
152152
read_only: bool = False
153153
write_only: bool = False
154+
use_frozen_field: bool = False
154155

155156
if not TYPE_CHECKING:
156157
if not PYDANTIC_V2:

src/datamodel_code_generator/model/pydantic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Config(_BaseModel):
3535
allow_mutation: Optional[bool] = None # noqa: UP045
3636
arbitrary_types_allowed: Optional[bool] = None # noqa: UP045
3737
orm_mode: Optional[bool] = None # noqa: UP045
38+
validate_assignment: Optional[bool] = None # noqa: UP045
3839

3940

4041
__all__ = [

src/datamodel_code_generator/model/pydantic/base_model.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def _process_data_in_str(self, data: dict[str, Any]) -> None:
154154
if self.const:
155155
data["const"] = True
156156

157+
if self.use_frozen_field and self.read_only:
158+
data["allow_mutation"] = False
159+
157160
def _process_annotated_field_arguments(self, field_arguments: list[str]) -> list[str]: # noqa: PLR6301
158161
return field_arguments
159162

@@ -294,7 +297,7 @@ class BaseModel(BaseModelBase):
294297
TEMPLATE_FILE_PATH: ClassVar[str] = "pydantic/BaseModel.jinja2"
295298
BASE_CLASS: ClassVar[str] = "pydantic.BaseModel"
296299

297-
def __init__( # noqa: PLR0913
300+
def __init__( # noqa: PLR0912, PLR0913
298301
self,
299302
*,
300303
reference: Reference,
@@ -348,6 +351,12 @@ def __init__( # noqa: PLR0913
348351
for config_attribute in "allow_population_by_field_name", "allow_mutation":
349352
if config_attribute in self.extra_template_data:
350353
config_parameters[config_attribute] = self.extra_template_data[config_attribute]
354+
355+
if "validate_assignment" not in config_parameters and any(
356+
field.use_frozen_field and field.read_only for field in self.fields
357+
):
358+
config_parameters["validate_assignment"] = True
359+
351360
for data_type in self.all_data_types:
352361
if data_type.is_custom_type: # pragma: no cover
353362
config_parameters["arbitrary_types_allowed"] = True

src/datamodel_code_generator/model/pydantic_v2/base_model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ def _process_data_in_str(self, data: dict[str, Any]) -> None:
136136
# unique_items is not supported in pydantic 2.0
137137
data.pop("unique_items", None)
138138

139+
if self.use_frozen_field and self.read_only:
140+
data["frozen"] = True
141+
139142
if "union_mode" in data:
140143
if self.data_type.is_union:
141144
data["union_mode"] = data.pop("union_mode").value

src/datamodel_code_generator/parser/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,7 @@ def __init__( # noqa: PLR0913, PLR0915
567567
keyword_only: bool = False,
568568
frozen_dataclasses: bool = False,
569569
no_alias: bool = False,
570+
use_frozen_field: bool = False,
570571
formatters: list[Formatter] = DEFAULT_FORMATTERS,
571572
parent_scoped_naming: bool = False,
572573
dataclass_arguments: DataclassArguments | None = None,
@@ -704,6 +705,7 @@ def __init__( # noqa: PLR0913, PLR0915
704705
self.formatters: list[Formatter] = formatters
705706
self.type_mappings: dict[tuple[str, str], str] = Parser._parse_type_mappings(type_mappings)
706707
self.read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = read_only_write_only_model_type
708+
self.use_frozen_field: bool = use_frozen_field
707709

708710
@property
709711
def field_name_model_type(self) -> ModelType:

0 commit comments

Comments
 (0)