Skip to content
Merged
2 changes: 1 addition & 1 deletion docs/cli-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ This documentation is auto-generated from test cases.

- [`--no-alias`](field-customization.md#no-alias)
- [`--no-color`](utility-options.md#no-color)
- [`--no-treat-dot-as-module`](template-customization.md#no-treat-dot-as-module)
- [`--no-use-specialized-enum`](typing-customization.md#no-use-specialized-enum)
- [`--no-use-standard-collections`](typing-customization.md#no-use-standard-collections)
- [`--no-use-union-operator`](typing-customization.md#no-use-union-operator)
Expand Down Expand Up @@ -150,7 +151,6 @@ This documentation is auto-generated from test cases.
### T {#t}

- [`--target-python-version`](model-customization.md#target-python-version)
- [`--treat-dot-as-module`](template-customization.md#treat-dot-as-module)
- [`--type-mappings`](typing-customization.md#type-mappings)

### U {#u}
Expand Down
4 changes: 2 additions & 2 deletions docs/cli-reference/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ datamodel-codegen [OPTIONS]
| [`--enable-version-header`](template-customization.md#enable-version-header) | Include tool version information in file header. |
| [`--extra-template-data`](template-customization.md#extra-template-data) | Pass custom template variables from JSON file for code generation. |
| [`--formatters`](template-customization.md#formatters) | Specify code formatters to apply to generated output. |
| [`--treat-dot-as-module`](template-customization.md#treat-dot-as-module) | Treat dots in schema names as module separators. |
| [`--no-treat-dot-as-module`](template-customization.md#no-treat-dot-as-module) | Keep dots in schema names as underscores for flat output. |
| [`--use-double-quotes`](template-customization.md#use-double-quotes) | Use double quotes for string literals in generated code. |
| [`--use-exact-imports`](template-customization.md#use-exact-imports) | Import exact types instead of modules. |
| [`--wrap-string-literal`](template-customization.md#wrap-string-literal) | Wrap long string literals across multiple lines. |
Expand Down Expand Up @@ -225,6 +225,7 @@ All options sorted alphabetically:
- [`--module-split-mode`](general-options.md#module-split-mode) - Split generated models into separate files, one per model cl...
- [`--no-alias`](field-customization.md#no-alias) - Disable Field alias generation for non-Python-safe property ...
- [`--no-color`](utility-options.md#no-color) - Disable colorized output
- [`--no-treat-dot-as-module`](template-customization.md#no-treat-dot-as-module) - Keep dots in schema names as underscores for flat output.
- [`--no-use-specialized-enum`](typing-customization.md#no-use-specialized-enum) - Disable specialized Enum classes for Python 3.11+ code gener...
- [`--no-use-standard-collections`](typing-customization.md#no-use-standard-collections) - Use built-in dict/list instead of typing.Dict/List.
- [`--no-use-union-operator`](typing-customization.md#no-use-union-operator) - Test GraphQL annotated types with standard collections and u...
Expand All @@ -248,7 +249,6 @@ All options sorted alphabetically:
- [`--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-python-version`](model-customization.md#target-python-version) - Target Python version for generated code syntax and imports.
- [`--treat-dot-as-module`](template-customization.md#treat-dot-as-module) - Treat dots in schema names as module separators.
- [`--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_...
- [`--url`](base-options.md#url) - Fetch schema from URL with custom HTTP headers.
Expand Down
19 changes: 7 additions & 12 deletions docs/cli-reference/template-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
| [`--enable-version-header`](#enable-version-header) | Include tool version information in file header. |
| [`--extra-template-data`](#extra-template-data) | Pass custom template variables from JSON file for code gener... |
| [`--formatters`](#formatters) | Specify code formatters to apply to generated output. |
| [`--treat-dot-as-module`](#treat-dot-as-module) | Treat dots in schema names as module separators. |
| [`--no-treat-dot-as-module`](#no-treat-dot-as-module) | Keep dots in schema names as underscores for flat output. |
| [`--use-double-quotes`](#use-double-quotes) | Use double quotes for string literals in generated code. |
| [`--use-exact-imports`](#use-exact-imports) | Import exact types instead of modules. |
| [`--wrap-string-literal`](#wrap-string-literal) | Wrap long string literals across multiple lines. |
Expand Down Expand Up @@ -2260,21 +2260,21 @@ Use this to customize formatting or disable formatters entirely.

---

## `--treat-dot-as-module` {#treat-dot-as-module}
## `--no-treat-dot-as-module` {#no-treat-dot-as-module}

Treat dots in schema names as module separators.
Keep dots in schema names as underscores for flat output.

The `--treat-dot-as-module` flag configures the code generation behavior.
The `--no-treat-dot-as-module` flag prevents splitting dotted schema names.

**See also:** [Module Structure and Exports](../module-exports.md)

!!! tip "Usage"

```bash
datamodel-codegen --input schema.json --treat-dot-as-module # (1)!
datamodel-codegen --input schema.json --no-treat-dot-as-module # (1)!
```

1. :material-arrow-left: `--treat-dot-as-module` - the option documented here
1. :material-arrow-left: `--no-treat-dot-as-module` - the option documented here

??? example "Examples"

Expand Down Expand Up @@ -2306,12 +2306,7 @@ The `--treat-dot-as-module` flag configures the code generation behavior.
# filename: treat_dot_as_module_single
# timestamp: 2019-07-26T00:00:00+00:00

# model/__init__.py
# generated by datamodel-codegen:
# filename: treat_dot_as_module_single
# timestamp: 2019-07-26T00:00:00+00:00

# model/schema.py
# model_schema.py
# generated by datamodel-codegen:
# filename: model.schema.json
# timestamp: 2019-07-26T00:00:00+00:00
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 @@ -465,7 +465,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
custom_formatters_kwargs: dict[str, Any] | None = None,
use_pendulum: bool = False,
http_query_parameters: Sequence[tuple[str, str]] | None = None,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
use_exact_imports: bool = False,
union_mode: UnionMode | None = None,
output_datetime_class: DatetimeClassType | None = None,
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
custom_formatters_kwargs: Optional[TextIOBase] = None # noqa: UP045
use_pendulum: bool = False
http_query_parameters: Optional[Sequence[tuple[str, str]]] = None # noqa: UP045
treat_dot_as_module: bool = False
treat_dot_as_module: Optional[bool] = None # noqa: UP045
use_exact_imports: bool = False
union_mode: Optional[UnionMode] = None # noqa: UP045
output_datetime_class: Optional[DatetimeClassType] = None # noqa: UP045
Expand Down
5 changes: 3 additions & 2 deletions src/datamodel_code_generator/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,9 @@ def start_section(self, heading: str | None) -> None:
)
model_options.add_argument(
"--treat-dot-as-module",
help="treat dotted module names as modules",
action="store_true",
help="Treat dotted schema names as module paths, creating nested directory structures (e.g., 'foo.bar.Model' "
"becomes 'foo/bar.py'). Use --no-treat-dot-as-module to keep dots in names as underscores for single-file output.",
action=BooleanOptionalAction,
default=None,
)
model_options.add_argument(
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/cli_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class CLIOptionMeta:
"--disable-appending-item-suffix": CLIOptionMeta(
name="--disable-appending-item-suffix", category=OptionCategory.TEMPLATE
),
"--treat-dot-as-module": CLIOptionMeta(name="--treat-dot-as-module", category=OptionCategory.TEMPLATE),
"--no-treat-dot-as-module": CLIOptionMeta(name="--no-treat-dot-as-module", category=OptionCategory.TEMPLATE),
"--disable-timestamp": CLIOptionMeta(name="--disable-timestamp", category=OptionCategory.TEMPLATE),
"--enable-version-header": CLIOptionMeta(name="--enable-version-header", category=OptionCategory.TEMPLATE),
"--enable-command-header": CLIOptionMeta(name="--enable-command-header", category=OptionCategory.TEMPLATE),
Expand Down
35 changes: 24 additions & 11 deletions src/datamodel_code_generator/model/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,28 +406,41 @@ def get_template(template_file_path: Path) -> Template:
return environment.get_template(template_file_path.name)


def sanitize_module_name(name: str, *, treat_dot_as_module: bool) -> str:
"""Sanitize a module name by replacing invalid characters."""
def sanitize_module_name(name: str, *, treat_dot_as_module: bool | None) -> str:
"""Sanitize a module name by replacing invalid characters.

If treat_dot_as_module is True, dots are preserved in the name.
If treat_dot_as_module is False or None (default), dots are replaced with underscores.
"""
pattern = r"[^0-9a-zA-Z_.]" if treat_dot_as_module else r"[^0-9a-zA-Z_]"
sanitized = re.sub(pattern, "_", name)
if sanitized and sanitized[0].isdigit():
sanitized = f"_{sanitized}"
return sanitized


def get_module_path(name: str, file_path: Path | None, *, treat_dot_as_module: bool) -> list[str]:
"""Get the module path components from a name and file path."""
def get_module_path(name: str, file_path: Path | None, *, treat_dot_as_module: bool | None) -> list[str]:
"""Get the module path components from a name and file path.

The treat_dot_as_module flag controls behavior:
- None (default): Split names on dots (backward compat), but sanitize file names (replace dots)
- True: Split names on dots AND keep dots in file names (for modular output)
- False: Don't split names on dots AND sanitize file names (new feature for flat output)
"""
should_split_names = treat_dot_as_module is not False
should_keep_dots_in_files = treat_dot_as_module is True
if file_path:
sanitized_stem = sanitize_module_name(file_path.stem, treat_dot_as_module=treat_dot_as_module)
sanitized_stem = sanitize_module_name(file_path.stem, treat_dot_as_module=should_keep_dots_in_files)
module_parts = name.split(".")[:-1] if should_split_names else []
return [
*file_path.parts[:-1],
sanitized_stem,
*name.split(".")[:-1],
*module_parts,
]
return name.split(".")[:-1]
return name.split(".")[:-1] if should_split_names else []


def get_module_name(name: str, file_path: Path | None, *, treat_dot_as_module: bool) -> str:
def get_module_name(name: str, file_path: Path | None, *, treat_dot_as_module: bool | None) -> str:
"""Get the full module name from a name and file path."""
return ".".join(get_module_path(name, file_path, treat_dot_as_module=treat_dot_as_module))

Expand Down Expand Up @@ -501,7 +514,7 @@ def __init__( # noqa: PLR0913
nullable: bool = False,
keyword_only: bool = False,
frozen: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
dataclass_arguments: DataclassArguments | None = None,
) -> None:
"""Initialize a data model with fields, base classes, and configuration."""
Expand Down Expand Up @@ -561,7 +574,7 @@ def __init__( # noqa: PLR0913
self._additional_imports.extend(self.DEFAULT_IMPORTS)
self.default: Any = default
self._nullable: bool = nullable
self._treat_dot_as_module: bool = treat_dot_as_module
self._treat_dot_as_module: bool | None = treat_dot_as_module

def _validate_fields(self, fields: list[DataModelFieldBase]) -> list[DataModelFieldBase]:
names: set[str] = set()
Expand Down Expand Up @@ -721,7 +734,7 @@ def create_base_class_model(
reference: Reference, # noqa: ARG003
custom_template_dir: Path | None = None, # noqa: ARG003
keyword_only: bool = False, # noqa: ARG003, FBT001, FBT002
treat_dot_as_module: bool = False, # noqa: ARG003, FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: ARG003, FBT001
) -> DataModel | None:
"""Create a shared base class model for DRY configuration.

Expand Down
4 changes: 2 additions & 2 deletions src/datamodel_code_generator/model/dataclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__( # noqa: PLR0913
nullable: bool = False,
keyword_only: bool = False,
frozen: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
dataclass_arguments: DataclassArguments | None = None,
) -> None:
"""Initialize dataclass with fields sorted by field assignment requirement."""
Expand Down Expand Up @@ -218,7 +218,7 @@ def __init__( # noqa: PLR0913, PLR0917
use_union_operator: bool = False, # noqa: FBT001, FBT002
use_pendulum: bool = False, # noqa: FBT001, FBT002
target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime,
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
use_serialize_as_any: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize type manager with datetime type mapping."""
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize Enum with optional specialized base class based on type."""
super().__init__(
Expand Down
6 changes: 3 additions & 3 deletions src/datamodel_code_generator/model/msgspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize msgspec Struct with fields sorted by field assignment requirement."""
super().__init__(
Expand Down Expand Up @@ -174,7 +174,7 @@ def create_base_class_model(
reference: Reference,
custom_template_dir: Path | None = None,
keyword_only: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
) -> Struct | None:
"""Create a shared base class model for DRY configuration.

Expand Down Expand Up @@ -504,7 +504,7 @@ def __init__( # noqa: PLR0913, PLR0917
use_union_operator: bool = False, # noqa: FBT001, FBT002
use_pendulum: bool = False, # noqa: FBT001, FBT002
target_datetime_class: DatetimeClassType | None = None,
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
use_serialize_as_any: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize type manager with optional datetime type mapping."""
Expand Down
4 changes: 2 additions & 2 deletions src/datamodel_code_generator/model/pydantic/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize the BaseModel with fields and configuration."""
methods: list[str] = [field.method for field in fields if field.method]
Expand Down Expand Up @@ -345,7 +345,7 @@ def __init__( # noqa: PLR0912, PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize the BaseModel with Config and extra fields support."""
super().__init__(
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/pydantic/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def __init__( # noqa: PLR0913, PLR0917
use_union_operator: bool = False, # noqa: FBT001, FBT002
use_pendulum: bool = False, # noqa: FBT001, FBT002
target_datetime_class: DatetimeClassType | None = None,
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
use_serialize_as_any: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize the DataTypeManager with Pydantic v1 type mappings."""
Expand Down
4 changes: 2 additions & 2 deletions src/datamodel_code_generator/model/pydantic_v2/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize BaseModel with ConfigDict generation from template data."""
super().__init__(
Expand Down Expand Up @@ -285,7 +285,7 @@ def create_base_class_model(
reference: Reference,
custom_template_dir: Path | None = None,
keyword_only: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
) -> BaseModel | None:
"""Create a shared base class model for DRY configuration.

Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/pydantic_v2/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__( # noqa: PLR0913, PLR0917
use_union_operator: bool = False, # noqa: FBT001, FBT002
use_pendulum: bool = False, # noqa: FBT001, FBT002
target_datetime_class: DatetimeClassType | None = None,
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
use_serialize_as_any: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize with pydantic v2-specific DataType."""
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize GraphQL scalar type with Python type mapping."""
extra_template_data = extra_template_data or defaultdict(dict)
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/typed_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize TypedDict model."""
super().__init__(
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def __init__( # noqa: PLR0913, PLR0917
use_union_operator: bool = False, # noqa: FBT001, FBT002
use_pendulum: bool = False, # noqa: FBT001, FBT002
target_datetime_class: DatetimeClassType | None = None,
treat_dot_as_module: bool = False, # noqa: FBT001, FBT002
treat_dot_as_module: bool | None = None, # noqa: FBT001
use_serialize_as_any: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize type manager with basic type mapping."""
Expand Down
2 changes: 1 addition & 1 deletion src/datamodel_code_generator/model/union.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def __init__( # noqa: PLR0913
default: Any = UNDEFINED,
nullable: bool = False,
keyword_only: bool = False,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
) -> None:
"""Initialize GraphQL union type."""
super().__init__(
Expand Down
4 changes: 2 additions & 2 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
custom_formatters_kwargs: dict[str, Any] | None = None,
use_pendulum: bool = False,
http_query_parameters: Sequence[tuple[str, str]] | None = None,
treat_dot_as_module: bool = False,
treat_dot_as_module: bool | None = None,
use_exact_imports: bool = False,
default_field_extras: dict[str, Any] | None = None,
target_datetime_class: DatetimeClassType | None = None,
Expand Down Expand Up @@ -784,7 +784,7 @@ def __init__( # noqa: PLR0912, PLR0913, PLR0915
use_union_operator=use_union_operator,
use_pendulum=use_pendulum,
target_datetime_class=target_datetime_class,
treat_dot_as_module=treat_dot_as_module,
treat_dot_as_module=treat_dot_as_module or False,
use_serialize_as_any=use_serialize_as_any,
)
self.data_model_type: type[DataModel] = data_model_type
Expand Down
Loading
Loading