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
32 changes: 32 additions & 0 deletions src/datamodel_code_generator/parser/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1125,6 +1125,38 @@ def __delete_duplicate_models(self, models: list[DataModel]) -> None: # noqa: P
{f"{c.module_name}.{c.type_hint}": c for c in child.base_classes}.values()
)
models_to_remove.add(duplicate_model)

if self.reuse_model and self.collapse_reuse_models:
max_iterations, iteration = len(models), 0
while True:
iteration += 1
if iteration > max_iterations: # pragma: no cover
msg = f"Deduplication exceeded max iterations ({max_iterations})"
raise RuntimeError(msg)

content_key_to_models: dict[tuple[Any, ...], list[DataModel]] = defaultdict(list)
for model in models:
if model not in models_to_remove and not isinstance(model, self.data_model_root_type):
model._dedup_key_cache.clear() # noqa: SLF001
content_key_to_models[model.get_dedup_key(None, use_default=True)].append(model)

if not (
duplicates := [
(canonical := group[0], dup)
for group in content_key_to_models.values()
if len(group) > 1
for dup in group[1:]
if dup not in models_to_remove
]
):
break

for canonical, duplicate in duplicates:
duplicate.replace_children_in_models(models, canonical.reference)
for child in duplicate.reference.iter_data_model_children(): # pragma: no cover
child.base_classes = list({c.reference: c for c in child.base_classes}.values())
models_to_remove.add(duplicate)

# Batch removal: O(n) instead of O(n²)
if models_to_remove:
models[:] = [m for m in models if m not in models_to_remove]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# generated by datamodel-codegen:
# filename: reuse_model_inline_definitions.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Pos(BaseModel):
start: int | None = None
end: int | None = None


class Node1(BaseModel):
pos: Pos | None = None


class Model(BaseModel):
node1: Node1 | None = None
node2: Node1 | None = None
node3: Node1 | None = None
26 changes: 26 additions & 0 deletions tests/data/expected/main/jsonschema/reuse_model_collapse_nested.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# generated by datamodel-codegen:
# filename: reuse_model_collapse_nested.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel


class Start(BaseModel):
line: int | None = None
col: int | None = None


class Pos(BaseModel):
start: Start | None = None
end: Start | None = None


class Block1(BaseModel):
pos: Pos | None = None


class Model(BaseModel):
block1: Block1 | None = None
block2: Block1 | None = None
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# generated by datamodel-codegen:
# filename: reuse_model_collapse_with_root.json
# timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from pydantic import BaseModel, RootModel


class Pos(BaseModel):
x: int | None = None
y: int | None = None


class Nested1(BaseModel):
pos: Pos | None = None


class StringType(RootModel[str]):
root: str


class Model(BaseModel):
field1: StringType | None = None
field2: StringType | None = None
nested1: Nested1 | None = None
nested2: Nested1 | None = None
54 changes: 54 additions & 0 deletions tests/data/jsonschema/reuse_model_collapse_nested.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"block1": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"start": {
"type": "object",
"properties": {
"line": {"type": "integer"},
"col": {"type": "integer"}
}
},
"end": {
"type": "object",
"properties": {
"line": {"type": "integer"},
"col": {"type": "integer"}
}
}
}
}
}
},
"block2": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"start": {
"type": "object",
"properties": {
"line": {"type": "integer"},
"col": {"type": "integer"}
}
},
"end": {
"type": "object",
"properties": {
"line": {"type": "integer"},
"col": {"type": "integer"}
}
}
}
}
}
}
}
}
41 changes: 41 additions & 0 deletions tests/data/jsonschema/reuse_model_collapse_with_root.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"definitions": {
"StringType": {
"type": "string"
}
},
"properties": {
"field1": {
"$ref": "#/definitions/StringType"
},
"field2": {
"$ref": "#/definitions/StringType"
},
"nested1": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"x": {"type": "integer"},
"y": {"type": "integer"}
}
}
}
},
"nested2": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"x": {"type": "integer"},
"y": {"type": "integer"}
}
}
}
}
}
}
42 changes: 42 additions & 0 deletions tests/data/jsonschema/reuse_model_inline_definitions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "http://json-schema.org/draft-07/schema",
"type": "object",
"properties": {
"node1": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"start": {"type": "integer"},
"end": {"type": "integer"}
}
}
}
},
"node2": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"start": {"type": "integer"},
"end": {"type": "integer"}
}
}
}
},
"node3": {
"type": "object",
"properties": {
"pos": {
"type": "object",
"properties": {
"start": {"type": "integer"},
"end": {"type": "integer"}
}
}
}
}
}
}
48 changes: 48 additions & 0 deletions tests/main/jsonschema/test_main_jsonschema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1132,6 +1132,54 @@ def test_main_json_reuse_enum(output_file: Path) -> None:
)


def test_main_reuse_model_collapse_inline_definitions(output_file: Path) -> None:
"""Test --reuse-model --collapse-reuse-models deduplicates identical inline definitions."""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "reuse_model_inline_definitions.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
extra_args=[
"--reuse-model",
"--collapse-reuse-models",
"--output-model-type",
"pydantic_v2.BaseModel",
],
)


def test_main_reuse_model_collapse_with_root(output_file: Path) -> None:
"""Test --reuse-model --collapse-reuse-models skips RootModel deduplication."""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "reuse_model_collapse_with_root.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
extra_args=[
"--reuse-model",
"--collapse-reuse-models",
"--output-model-type",
"pydantic_v2.BaseModel",
],
)


def test_main_reuse_model_collapse_nested(output_file: Path) -> None:
"""Test --reuse-model --collapse-reuse-models with deeply nested identical structures."""
run_main_and_assert(
input_path=JSON_SCHEMA_DATA_PATH / "reuse_model_collapse_nested.json",
output_path=output_file,
input_file_type="jsonschema",
assert_func=assert_file_content,
extra_args=[
"--reuse-model",
"--collapse-reuse-models",
"--output-model-type",
"pydantic_v2.BaseModel",
],
)


@pytest.mark.cli_doc(
options=["--capitalize-enum-members"],
option_description="""Capitalize enum member names to UPPER_CASE format.
Expand Down
Loading