Repository: koxudaxi/datamodel-code-generator

Issue:
Model configuration got mixed up when objects with the same name in different schemas are defined
**Describe the bug**
I want a Pydantic V2 model.

- I define `allowedAnimals` in `aaaschema.json` as an array of objects
- I generate the code. A RootModel is generated and it works.
- I define `allowedAnimals` as object with `additionalProperties: false` in `bug.json`
- I generate the code. The model of the first schema has been edited and now the RootModel's config has `extra: forbid` (which is not valid)
- I rename `allowedAnimals` in `bug.json` with a different name
- Now the generated code of `aaaschema.json` is correct again.

**To Reproduce**

See: https://github.com/ilovelinux/datamodel-code-generator-issue-2460

**Expected behavior**
Model defined in `bug.json` should not interfere with model generation of `aaaschema.json` since they are completely different files.

**Version:**
 - OS: Linux
 - Python version: 3.13.5
 - datamodel-code-generator version: 0.32.0

**Additional context**
If I rename `aaaschema.json` to `schema.json`, it works correctly. I guess that it's because of the alphabetic order

May be related to #2169 



Discussion:
In `extra_template_data` we use `name` as identifier, which is not unique among schemes (and may not be unique in a single scheme either)

https://github.com/koxudaxi/datamodel-code-generator/blob/0070c1c13e5c61c705c9679db7255db1b866b18c/src/datamodel_code_generator/parser/jsonschema.py#L656-L662

Because of that, `additionalProperties: false` in `boo.json#/$defs/allowedAnimals` is used in `aaaschema.json#/$defs/allowedAnimals` since both the objects are named `allowedAnimals` 

---

I tried to use the full path as identifier (`boo.json#/$defs/allowedAnimals`, `aaaschema.json#/$defs/allowedAnimals`). It works but it also breaks the current implementation of `--extra-template-data` since object identifiers should now be paths and not just names. It actually makes sense since the user may want to have some extra template data for just one of the two objects, but it's still a breaking change.

---

Right now this bug is affecting `jsonschema`, `openapi` and maybe `graphql` (I'm not sure about it since I'm not sure whether names are unique or not)
I'm considering to allow to use both object names and paths for `--extra-template-data`, this way it would not be breaking.




Patch:
diff --git a/README.md b/README.md
index 56fd94660..1764ca447 100644
--- a/README.md
+++ b/README.md
@@ -493,7 +493,12 @@ Template customization:
                         Custom template directory
   --encoding ENCODING   The encoding of input and output (default: utf-8)
   --extra-template-data EXTRA_TEMPLATE_DATA
-                        Extra template data
+                        Extra template data for output models. Input is supposed to be a
+                        json/yaml file. For OpenAPI and Jsonschema the keys are the spec
+                        path of the object, or the name of the object if you want to apply
+                        the template data to multiple objects with the same name. If you are
+                        using another input file type (e.g. GraphQL), the key is the name of
+                        the object. The value is a dictionary of the template data to add.
   --use-double-quotes   Model generated with double quotes. Single quotes or your black
                         config skip_string_normalization value will be used without this
                         option.
diff --git a/docs/index.md b/docs/index.md
index 08022ecab..1b32d9a92 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -485,7 +485,12 @@ Template customization:
                         Custom template directory
   --encoding ENCODING   The encoding of input and output (default: utf-8)
   --extra-template-data EXTRA_TEMPLATE_DATA
-                        Extra template data
+                        Extra template data for output models. Input is supposed to be a
+                        json/yaml file. For OpenAPI and Jsonschema the keys are the spec
+                        path of the object, or the name of the object if you want to apply
+                        the template data to multiple objects with the same name. If you are
+                        using another input file type (e.g. GraphQL), the key is the name of
+                        the object. The value is a dictionary of the template data to add.
   --use-double-quotes   Model generated with double quotes. Single quotes or your black
                         config skip_string_normalization value will be used without this
                         option.
diff --git a/src/datamodel_code_generator/arguments.py b/src/datamodel_code_generator/arguments.py
index 8271e21b9..f28c77e2d 100644
--- a/src/datamodel_code_generator/arguments.py
+++ b/src/datamodel_code_generator/arguments.py
@@ -437,7 +437,11 @@ def start_section(self, heading: str | None) -> None:
 )
 template_options.add_argument(
     "--extra-template-data",
-    help="Extra template data",
+    help="Extra template data for output models. Input is supposed to be a json/yaml file. "
+    "For OpenAPI and Jsonschema the keys are the spec path of the object, or the name of the object if you want to "
+    "apply the template data to multiple objects with the same name. "
+    "If you are using another input file type (e.g. GraphQL), the key is the name of the object. "
+    "The value is a dictionary of the template data to add.",
     type=FileType("rt"),
 )
 template_options.add_argument(
diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py
index fdaec5136..2c1bdd5cc 100644
--- a/src/datamodel_code_generator/model/base.py
+++ b/src/datamodel_code_generator/model/base.py
@@ -298,11 +298,18 @@ def __init__(  # noqa: PLR0913
 
         self.reference.source = self
 
-        self.extra_template_data = (
+        if extra_template_data is not None:
             # The supplied defaultdict will either create a new entry,
             # or already contain a predefined entry for this type
-            extra_template_data[self.name] if extra_template_data is not None else defaultdict(dict)
-        )
+            self.extra_template_data = extra_template_data[self.reference.path]
+
+            # We use the full object reference path as dictionary key, but
+            # we still support `name` as key because it was used for
+            # `--extra-template-data` input file and we don't want to break the
+            # existing behavior.
+            self.extra_template_data.update(extra_template_data[self.name])
+        else:
+            self.extra_template_data = defaultdict(dict)
 
         self.fields = self._validate_fields(fields) if fields else []
 
diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py
index c7b5b0cdf..604ec0c7a 100644
--- a/src/datamodel_code_generator/parser/jsonschema.py
+++ b/src/datamodel_code_generator/parser/jsonschema.py
@@ -654,13 +654,13 @@ def get_ref_data_type(self, ref: str) -> DataType:
         reference = self.model_resolver.add_ref(ref)
         return self.data_type(reference=reference)
 
-    def set_additional_properties(self, name: str, obj: JsonSchemaObject) -> None:
+    def set_additional_properties(self, path: str, obj: JsonSchemaObject) -> None:
         if isinstance(obj.additionalProperties, bool):
-            self.extra_template_data[name]["additionalProperties"] = obj.additionalProperties
+            self.extra_template_data[path]["additionalProperties"] = obj.additionalProperties
 
-    def set_title(self, name: str, obj: JsonSchemaObject) -> None:
+    def set_title(self, path: str, obj: JsonSchemaObject) -> None:
         if obj.title:
-            self.extra_template_data[name]["title"] = obj.title
+            self.extra_template_data[path]["title"] = obj.title
 
     def _deep_merge(self, dict1: dict[Any, Any], dict2: dict[Any, Any]) -> dict[Any, Any]:
         result = dict1.copy()
@@ -782,7 +782,7 @@ def _parse_object_common_part(  # noqa: PLR0913, PLR0917
         if self.use_title_as_name and obj.title:  # pragma: no cover
             name = obj.title
         reference = self.model_resolver.add(path, name, class_name=True, loaded=True)
-        self.set_additional_properties(reference.name, obj)
+        self.set_additional_properties(reference.path, obj)
 
         data_model_type = self._create_data_model(
             reference=reference,
@@ -993,7 +993,7 @@ def parse_object(
             loaded=True,
         )
         class_name = reference.name
-        self.set_title(class_name, obj)
+        self.set_title(reference.path, obj)
         fields = self.parse_object_fields(
             obj, path, get_module_name(class_name, None, treat_dot_as_module=self.treat_dot_as_module)
         )
@@ -1022,7 +1022,7 @@ def parse_object(
             )
             data_model_type_class = self.data_model_root_type
 
-        self.set_additional_properties(class_name, obj)
+        self.set_additional_properties(reference.path, obj)
 
         data_model_type = self._create_data_model(
             model_type=data_model_type_class,
@@ -1305,8 +1305,8 @@ def parse_root_type(  # noqa: PLR0912
             name = obj.title
         if not reference:
             reference = self.model_resolver.add(path, name, loaded=True, class_name=True)
-        self.set_title(name, obj)
-        self.set_additional_properties(name, obj)
+        self.set_title(reference.path, obj)
+        self.set_additional_properties(reference.path, obj)
         data_model_root_type = self.data_model_root_type(
             reference=reference,
             fields=[


Test patch:
diff --git a/tests/data/expected/main/jsonschema/pattern_properties_by_reference.py b/tests/data/expected/main/jsonschema/pattern_properties_by_reference.py
index c2c0d5780..28002d5e9 100644
--- a/tests/data/expected/main/jsonschema/pattern_properties_by_reference.py
+++ b/tests/data/expected/main/jsonschema/pattern_properties_by_reference.py
@@ -17,6 +17,9 @@ class Config:
 
 
 class TextResponse(BaseModel):
+    class Config:
+        extra = Extra.forbid
+
     __root__: Dict[constr(regex=r'^[a-z]{1}[0-9]{1}$'), Any]
 
 
diff --git a/tests/data/expected/main/jsonschema/same_name_objects.py b/tests/data/expected/main/jsonschema/same_name_objects.py
new file mode 100644
index 000000000..e130b5157
--- /dev/null
+++ b/tests/data/expected/main/jsonschema/same_name_objects.py
@@ -0,0 +1,32 @@
+# generated by datamodel-codegen:
+#   filename:  same_name_objects.json
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import Any, List
+
+from pydantic import BaseModel, Extra
+
+
+class Model(BaseModel):
+    __root__: Any
+
+
+class Friends(BaseModel):
+    pass
+
+    class Config:
+        extra = Extra.forbid
+
+
+class FriendsModel(BaseModel):
+    __root__: List
+
+
+class Tst2(BaseModel):
+    __root__: FriendsModel
+
+
+class Tst1(BaseModel):
+    __root__: FriendsModel
diff --git a/tests/data/expected/main/openapi/same_name_objects.py b/tests/data/expected/main/openapi/same_name_objects.py
new file mode 100644
index 000000000..4f64ed19a
--- /dev/null
+++ b/tests/data/expected/main/openapi/same_name_objects.py
@@ -0,0 +1,43 @@
+# generated by datamodel-codegen:
+#   filename:  same_name_objects.yaml
+#   timestamp: 2019-07-26T00:00:00+00:00
+
+from __future__ import annotations
+
+from typing import List, Optional
+
+from pydantic import BaseModel, Extra
+
+
+class Pets(BaseModel):
+    pass
+
+    class Config:
+        extra = Extra.forbid
+
+
+class Pet(BaseModel):
+    id: int
+    name: str
+    tag: Optional[str] = None
+
+
+class Error(BaseModel):
+    code: int
+    message: str
+
+
+class Resolved(BaseModel):
+    resolved: Optional[List[str]] = None
+
+
+class PetsModel(BaseModel):
+    __root__: List[Pet]
+
+
+class Friends2(BaseModel):
+    __root__: PetsModel
+
+
+class Friends1(BaseModel):
+    __root__: PetsModel
diff --git a/tests/data/jsonschema/same_name_objects.json b/tests/data/jsonschema/same_name_objects.json
new file mode 100644
index 000000000..ddfc2373b
--- /dev/null
+++ b/tests/data/jsonschema/same_name_objects.json
@@ -0,0 +1,16 @@
+{
+  "$id": "https://example.com/same_name_objects.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "definitions": {
+    "friends": {
+      "type": "object",
+      "additionalProperties": false
+    },
+    "tst1": {
+        "$ref": "person.json#/properties/friends"
+    },
+    "tst2": {
+        "$ref": "person.json#/properties/friends"
+    }
+  }
+}
diff --git a/tests/data/openapi/same_name_objects.yaml b/tests/data/openapi/same_name_objects.yaml
new file mode 100644
index 000000000..2a1b73add
--- /dev/null
+++ b/tests/data/openapi/same_name_objects.yaml
@@ -0,0 +1,15 @@
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+components:
+  schemas:
+    Pets:
+      type: object
+      additionalProperties: false
+    Friends1:
+      $ref: "resolved_models.yaml#/components/schemas/Pets"
+    Friends2:
+      $ref: "resolved_models.yaml#/components/schemas/Pets"
\ No newline at end of file
diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py
index d59f3f13f..f4309ce25 100644
--- a/tests/main/jsonschema/test_main_jsonschema.py
+++ b/tests/main/jsonschema/test_main_jsonschema.py
@@ -3125,3 +3125,21 @@ def test_main_extra_fields(extra_fields: str, output_model: str, expected_output
     ])
     assert return_code == Exit.OK
     assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / expected_output).read_text()
+
+
+@freeze_time("2019-07-26")
+def test_main_jsonschema_same_name_objects(tmp_path: Path) -> None:
+    """
+    See: https://github.com/koxudaxi/datamodel-code-generator/issues/2460
+    """
+    output_file: Path = tmp_path / "output.py"
+    return_code: Exit = main([
+        "--input",
+        str(JSON_SCHEMA_DATA_PATH / "same_name_objects.json"),
+        "--output",
+        str(output_file),
+        "--input-file-type",
+        "jsonschema",
+    ])
+    assert return_code == Exit.OK
+    assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / "same_name_objects.py").read_text()
diff --git a/tests/main/openapi/test_main_openapi.py b/tests/main/openapi/test_main_openapi.py
index 81098be2f..0c80157bf 100644
--- a/tests/main/openapi/test_main_openapi.py
+++ b/tests/main/openapi/test_main_openapi.py
@@ -2502,3 +2502,18 @@ def test_main_openapi_extra_fields_forbid(tmp_path: Path) -> None:
     ])
     assert return_code == Exit.OK
     assert output_file.read_text() == (EXPECTED_OPENAPI_PATH / "additional_properties.py").read_text()
+
+
+@freeze_time("2019-07-26")
+def test_main_openapi_same_name_objects(tmp_path: Path) -> None:
+    output_file: Path = tmp_path / "output.py"
+    return_code: Exit = main([
+        "--input",
+        str(OPEN_API_DATA_PATH / "same_name_objects.yaml"),
+        "--output",
+        str(output_file),
+        "--input-file-type",
+        "openapi",
+    ])
+    assert return_code == Exit.OK
+    assert output_file.read_text() == (EXPECTED_OPENAPI_PATH / "same_name_objects.py").read_text()