Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/cli-reference/field-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
|--------|-------------|
| [`--aliases`](#aliases) | Apply custom field and class name aliases from JSON file. |
| [`--capitalize-enum-members`](#capitalize-enum-members) | Capitalize enum member names to UPPER_CASE format. |
| [`--default-values`](#default-values) | Override field default values from external JSON file. |
| [`--empty-enum-field-name`](#empty-enum-field-name) | Name for empty string enum field values. |
| [`--extra-fields`](#extra-fields) | Configure how generated models handle extra fields not defin... |
| [`--field-constraints`](#field-constraints) | Generate Field() with validation constraints from schema. |
Expand Down Expand Up @@ -557,6 +558,74 @@ naming conventions for constants.

---

## `--default-values` {#default-values}

Override field default values from external JSON file.

The `--default-values` option allows specifying default values for fields via a JSON file.
Supports scoped format (ClassName.field) for hierarchical overrides.

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --default-values default_values/scoped_defaults.json # (1)!
```

1. :material-arrow-left: `--default-values` - the option documented here

??? example "Examples"

**Input Schema:**

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"User": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"status": {
"type": "string"
},
"page": {
"type": "integer"
}
},
"required": ["name"]
}
}
}
```

**Output:**

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

from __future__ import annotations

from typing import Any

from pydantic import BaseModel


class Model(BaseModel):
__root__: Any


class User(BaseModel):
name: str
status: str | None = 'active'
page: int | None = 1
```

---

## `--empty-enum-field-name` {#empty-enum-field-name}

Name for empty string enum field values.
Expand Down
3 changes: 2 additions & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) | 22 | Field naming and docstring behavior |
| 🏷️ [Field Customization](field-customization.md) | 23 | 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 |
Expand Down Expand Up @@ -61,6 +61,7 @@ This documentation is auto-generated from test cases.

- [`--dataclass-arguments`](model-customization.md#dataclass-arguments)
- [`--debug`](utility-options.md#debug)
- [`--default-values`](field-customization.md#default-values)
- [`--disable-appending-item-suffix`](template-customization.md#disable-appending-item-suffix)
- [`--disable-future-imports`](typing-customization.md#disable-future-imports)
- [`--disable-timestamp`](template-customization.md#disable-timestamp)
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 @@ -62,6 +62,7 @@ datamodel-codegen [OPTIONS]
|--------|-------------|
| [`--aliases`](field-customization.md#aliases) | Apply custom field and class name aliases from JSON file. |
| [`--capitalize-enum-members`](field-customization.md#capitalize-enum-members) | Capitalize enum member names to UPPER_CASE format. |
| [`--default-values`](field-customization.md#default-values) | Override field default values from external JSON file. |
| [`--empty-enum-field-name`](field-customization.md#empty-enum-field-name) | Name for empty string enum field values. |
| [`--extra-fields`](field-customization.md#extra-fields) | Configure how generated models handle extra fields not defined in schema. |
| [`--field-constraints`](field-customization.md#field-constraints) | Generate Field() with validation constraints from schema. |
Expand Down Expand Up @@ -232,6 +233,7 @@ All options sorted alphabetically:
- [`--custom-template-dir`](template-customization.md#custom-template-dir) - Use custom Jinja2 templates for model generation.
- [`--dataclass-arguments`](model-customization.md#dataclass-arguments) - Customize dataclass decorator arguments via JSON dictionary.
- [`--debug`](utility-options.md#debug) - Show debug messages during code generation
- [`--default-values`](field-customization.md#default-values) - Override field default values from external JSON file.
- [`--disable-appending-item-suffix`](template-customization.md#disable-appending-item-suffix) - Disable appending 'Item' suffix to array item types.
- [`--disable-future-imports`](typing-customization.md#disable-future-imports) - Prevent automatic addition of __future__ imports in generate...
- [`--disable-timestamp`](template-customization.md#disable-timestamp) - Disable timestamp in generated file header for reproducible ...
Expand Down
115 changes: 76 additions & 39 deletions src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import tempfile
import warnings
from collections import defaultdict
from collections.abc import Sequence # noqa: TC003 # pydantic needs it
from collections.abc import Callable, Sequence # noqa: TC003 # pydantic needs it
from enum import IntEnum
from io import TextIOBase
from pathlib import Path
Expand Down Expand Up @@ -176,7 +176,7 @@ def get_fields(cls) -> dict[str, Any]:
"""Get model fields."""
return cls.__fields__

@field_validator("aliases", "extra_template_data", "custom_formatters_kwargs", mode="before")
@field_validator("aliases", "extra_template_data", "custom_formatters_kwargs", "default_values", mode="before")
def validate_file(cls, value: Any) -> TextIOBase | None: # noqa: N805
"""Validate and open file path."""
if value is None: # pragma: no cover
Expand Down Expand Up @@ -503,6 +503,7 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) ->
snake_case_field: bool = False
strip_default_none: bool = False
aliases: Optional[TextIOBase] = None # noqa: UP045
default_values: Optional[TextIOBase] = None # noqa: UP045
disable_timestamp: bool = False
enable_version_header: bool = False
enable_command_header: bool = False
Expand Down Expand Up @@ -854,6 +855,38 @@ def generate_cli_command(config: dict[str, TomlValue]) -> str:
return " ".join(parts) + "\n"


def _load_json_config(
file_handle: TextIOBase | None,
name: str,
validator: Callable[[Any], str | None],
) -> tuple[dict[str, Any] | None, str | None]:
"""Load and validate a JSON configuration file.

Args:
file_handle: The file handle to read from, or None.
name: The name of the config for error messages.
validator: A function that validates the loaded data and returns an error message or None.

Returns:
A tuple of (loaded_dict, error_message). If successful, error_message is None.
If file_handle is None, returns (None, None).
"""
if file_handle is None:
return None, None

with file_handle as data:
try:
result = json.load(data)
except json.JSONDecodeError as e:
return None, f"Unable to load {name}: {e}"

error = validator(result)
if error:
return None, f"Unable to load {name}: {error}"

return result, None


def run_generate_from_config( # noqa: PLR0913, PLR0917
config: Config,
input_: Path | str | ParseResult,
Expand All @@ -863,6 +896,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
command_line: str | None,
custom_formatters_kwargs: dict[str, str] | None,
settings_path: Path | None = None,
default_value_overrides: dict[str, Any] | None = None,
) -> None:
"""Run code generation with the given config and parameters."""
result = generate(
Expand Down Expand Up @@ -990,6 +1024,7 @@ def run_generate_from_config( # noqa: PLR0913, PLR0917
all_exports_collision_strategy=config.all_exports_collision_strategy,
field_type_collision_strategy=config.field_type_collision_strategy,
module_split_mode=config.module_split_mode,
default_value_overrides=default_value_overrides,
)

if output is None and result is not None: # pragma: no cover
Expand Down Expand Up @@ -1175,46 +1210,45 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
else:
config.additional_imports = list(config.additional_imports) + additional_imports_from_template_data

if config.aliases is None:
aliases = None
else:
with config.aliases as data:
try:
aliases = json.load(data)
except json.JSONDecodeError as e:
print(f"Unable to load alias mapping: {e}", file=sys.stderr) # noqa: T201
return Exit.ERROR
if not isinstance(aliases, dict) or not all(
def _validate_aliases(data: Any) -> str | None:
if not isinstance(data, dict) or not all(
isinstance(k, str) and (isinstance(v, str) or (isinstance(v, list) and all(isinstance(i, str) for i in v)))
for k, v in aliases.items()
for k, v in data.items()
):
print( # noqa: T201
"Alias mapping must be a JSON mapping with string keys and string or list of strings values "
'(e.g. {"from": "to", "field": ["alias1", "alias2"]})',
file=sys.stderr,
return (
"must be a JSON mapping with string keys and string or list of strings values "
'(e.g. {"from": "to", "field": ["alias1", "alias2"]})'
)
return Exit.ERROR
return None

if config.custom_formatters_kwargs is None:
custom_formatters_kwargs = None
else:
with config.custom_formatters_kwargs as data:
try:
custom_formatters_kwargs = json.load(data)
except json.JSONDecodeError as e: # pragma: no cover
print( # noqa: T201
f"Unable to load custom_formatters_kwargs mapping: {e}",
file=sys.stderr,
)
return Exit.ERROR
if not isinstance(custom_formatters_kwargs, dict) or not all(
isinstance(k, str) and isinstance(v, str) for k, v in custom_formatters_kwargs.items()
): # pragma: no cover
print( # noqa: T201
'Custom formatters kwargs mapping must be a JSON string mapping (e.g. {"from": "to", ...})',
file=sys.stderr,
)
return Exit.ERROR
def _validate_string_key_dict(data: Any) -> str | None:
if not isinstance(data, dict) or not all(isinstance(k, str) for k in data):
return "must be a JSON object with string keys"
return None

def _validate_string_mapping(data: Any) -> str | None:
if not isinstance(data, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in data.items()):
return 'must be a JSON string mapping (e.g. {"key": "value", ...})'
return None

aliases, error = _load_json_config(config.aliases, "alias mapping", _validate_aliases)
if error:
print(error, file=sys.stderr) # noqa: T201
return Exit.ERROR

default_value_overrides, error = _load_json_config(
config.default_values, "default values mapping", _validate_string_key_dict
)
if error:
print(error, file=sys.stderr) # noqa: T201
return Exit.ERROR

custom_formatters_kwargs, error = _load_json_config(
config.custom_formatters_kwargs, "custom_formatters_kwargs mapping", _validate_string_mapping
)
if error:
print(error, file=sys.stderr) # noqa: T201
return Exit.ERROR

if config.check:
config_output = cast("Path", config.output)
Expand Down Expand Up @@ -1260,6 +1294,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
command_line=shlex.join(["datamodel-codegen", *args]) if config.enable_command_header else None,
custom_formatters_kwargs=custom_formatters_kwargs,
settings_path=config.output,
default_value_overrides=default_value_overrides,
)
except InvalidClassNameError as e:
print(f"{e} You have to set `--class-name` option", file=sys.stderr) # noqa: T201
Expand Down Expand Up @@ -1308,7 +1343,9 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
try:
from datamodel_code_generator.watch import watch_and_regenerate # noqa: PLC0415

return watch_and_regenerate(config, extra_template_data, aliases, custom_formatters_kwargs)
return watch_and_regenerate(
config, extra_template_data, aliases, custom_formatters_kwargs, default_value_overrides
)
except Exception as e: # noqa: BLE001
print(str(e), file=sys.stderr) # noqa: T201
return Exit.ERROR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,4 @@ class GenerateConfigDict(TypedDict):
all_exports_collision_strategy: NotRequired[AllExportsCollisionStrategy | None]
field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None]
module_split_mode: NotRequired[ModuleSplitMode | None]
default_value_overrides: NotRequired[Mapping[str, Any] | None]
1 change: 1 addition & 0 deletions src/datamodel_code_generator/_types/parser_config_dicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class ParserConfigDict(TypedDict):
read_only_write_only_model_type: NotRequired[ReadOnlyWriteOnlyModelType | None]
field_type_collision_strategy: NotRequired[FieldTypeCollisionStrategy | None]
target_pydantic_version: NotRequired[TargetPydanticVersion | None]
default_value_overrides: NotRequired[Mapping[str, Any] | None]


class GraphQLParserConfigDict(ParserConfigDict):
Expand Down
12 changes: 12 additions & 0 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,18 @@ def start_section(self, heading: str | None) -> None:
"Example: {'User.name': 'user_name', 'id': 'id_'} generates `id_: ... = Field(alias='id')`.",
type=Path,
)
template_options.add_argument(
"--default-values",
help="Default value overrides file (JSON). "
"Supports hierarchical formats: "
"Flat: {'field': value} applies to all occurrences. "
"Scoped: {'ClassName.field': value} applies to specific class. "
"Priority: scoped > flat. "
"Note: Scoped keys use the generated class name for JSON Schema/OpenAPI. "
"Required fields remain required unless --use-default is also specified. "
"Example: {'User.status': 'active', 'page': 1, 'limit': 10}",
type=Path,
)
template_options.add_argument(
"--custom-file-header",
help="Custom file header",
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 @@ -140,6 +140,7 @@ class CLIOptionMeta:
"--empty-enum-field-name": CLIOptionMeta(name="--empty-enum-field-name", category=OptionCategory.FIELD),
"--set-default-enum-member": CLIOptionMeta(name="--set-default-enum-member", category=OptionCategory.FIELD),
"--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-title-as-name": CLIOptionMeta(name="--use-title-as-name", category=OptionCategory.FIELD),
"--use-schema-description": CLIOptionMeta(name="--use-schema-description", category=OptionCategory.FIELD),
Expand Down
2 changes: 2 additions & 0 deletions src/datamodel_code_generator/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class Config:
all_exports_collision_strategy: AllExportsCollisionStrategy | None = None
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None
module_split_mode: ModuleSplitMode | None = None
default_value_overrides: Mapping[str, Any] | None = None


class ParserConfig(BaseModel):
Expand Down Expand Up @@ -330,6 +331,7 @@ class Config:
read_only_write_only_model_type: ReadOnlyWriteOnlyModelType | None = None
field_type_collision_strategy: FieldTypeCollisionStrategy | None = None
target_pydantic_version: TargetPydanticVersion | None = None
default_value_overrides: Mapping[str, Any] | None = None


class GraphQLParserConfig(ParserConfig):
Expand Down
1 change: 1 addition & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,7 @@ def __init__( # noqa: PLR0912, PLR0915
class_name_suffix=config.class_name_suffix,
class_name_affix_scope=config.class_name_affix_scope,
skip_affix_for_root=config.class_name is not None,
default_value_overrides=config.default_value_overrides,
)
self.class_name: str | None = config.class_name
self.wrap_string_literal: bool | None = config.wrap_string_literal
Expand Down
Loading
Loading