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=[ 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()