diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 44a483ec8..6368a9379 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -19,7 +19,7 @@ DataModelFieldBase, ) from datamodel_code_generator.model._types import WrappedDefault -from datamodel_code_generator.model.base import UNDEFINED +from datamodel_code_generator.model.base import UNDEFINED, repr_set_sorted from datamodel_code_generator.model.pydantic.imports import ( IMPORT_ANYURL, IMPORT_EXTRA, @@ -91,6 +91,8 @@ def validator(self) -> str | None: @property def field(self) -> str | None: """For backwards compatibility.""" + if self.is_class_var: + return None result = str(self) if ( self.use_default_kwarg @@ -250,17 +252,32 @@ def __str__(self) -> str: # noqa: PLR0912 elif self.required: field_arguments = ["...", *field_arguments] elif not default_factory: - from datamodel_code_generator.model.base import repr_set_sorted # noqa: PLC0415 - default_repr = repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) field_arguments = [default_repr, *field_arguments] + if self.is_class_var: + if self.default is UNDEFINED: # pragma: no cover + return "" + return repr_set_sorted(self.default) if isinstance(self.default, set) else repr(self.default) + return f"Field({', '.join(field_arguments)})" + @property + def is_class_var(self) -> bool: + """Check if this field is a ClassVar.""" + return self.extras.get("x-is-classvar") is True + + @property + def type_hint(self) -> str: + """Get the type hint including ClassVar if applicable.""" + if self.is_class_var: + return f"ClassVar[{super().type_hint}]" + return super().type_hint + @property def annotated(self) -> str | None: """Get the Annotated type hint if use_annotated is enabled.""" - if not self.use_annotated or not str(self): + if not self.use_annotated or not str(self) or self.is_class_var: return None return f"Annotated[{self.type_hint}, {self!s}]" diff --git a/src/datamodel_code_generator/model/pydantic_v2/base_model.py b/src/datamodel_code_generator/model/pydantic_v2/base_model.py index 6a4816742..fddd6e128 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -14,6 +14,7 @@ from datamodel_code_generator.imports import IMPORT_ANY, Import from datamodel_code_generator.model.base import ALL_MODEL, UNDEFINED, BaseClassDataType, DataModelFieldBase +from datamodel_code_generator.model.imports import IMPORT_CLASSVAR from datamodel_code_generator.model.pydantic.base_model import ( BaseModelBase, ) @@ -192,6 +193,8 @@ def imports(self) -> tuple[Import, ...]: """Get all required imports including AliasChoices and Field for discriminator.""" base_imports = super().imports extra_imports: list[Import] = [] + if self.is_class_var: + extra_imports.append(IMPORT_CLASSVAR) if self.validation_aliases: from datamodel_code_generator.model.pydantic_v2.imports import IMPORT_ALIAS_CHOICES # noqa: PLC0415 diff --git a/tests/data/expected/main/jsonschema/has_classvar_extra.py b/tests/data/expected/main/jsonschema/has_classvar_extra.py new file mode 100644 index 000000000..fc8129ea3 --- /dev/null +++ b/tests/data/expected/main/jsonschema/has_classvar_extra.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: has_classvar_extra.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import ClassVar + +from pydantic import BaseModel + + +class Model(BaseModel): + namespace: ClassVar[str | None] = 'test' diff --git a/tests/data/expected/main/jsonschema/has_classvar_extra_annotated.py b/tests/data/expected/main/jsonschema/has_classvar_extra_annotated.py new file mode 100644 index 000000000..789d8d7b2 --- /dev/null +++ b/tests/data/expected/main/jsonschema/has_classvar_extra_annotated.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: has_classvar_extra_annotated.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import ClassVar + +from pydantic import BaseModel + + +class Model(BaseModel): + namespace: ClassVar[str | None] = 'test' diff --git a/tests/data/expected/main/jsonschema/has_classvar_extra_set.py b/tests/data/expected/main/jsonschema/has_classvar_extra_set.py new file mode 100644 index 000000000..8c49b4f25 --- /dev/null +++ b/tests/data/expected/main/jsonschema/has_classvar_extra_set.py @@ -0,0 +1,13 @@ +# generated by datamodel-codegen: +# filename: has_classvar_extra_set.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import ClassVar + +from pydantic import BaseModel + + +class Model(BaseModel): + tags: ClassVar[set[str] | None] = {'a', 'b'} diff --git a/tests/data/jsonschema/has_classvar_extra.json b/tests/data/jsonschema/has_classvar_extra.json new file mode 100644 index 000000000..7c27e0e16 --- /dev/null +++ b/tests/data/jsonschema/has_classvar_extra.json @@ -0,0 +1,10 @@ +{ + "type": "object", + "properties": { + "namespace": { + "type": "string", + "x-is-classvar": true, + "default": "test" + } + } +} \ No newline at end of file diff --git a/tests/data/jsonschema/has_classvar_extra_annotated.json b/tests/data/jsonschema/has_classvar_extra_annotated.json new file mode 100644 index 000000000..0d4d5e04d --- /dev/null +++ b/tests/data/jsonschema/has_classvar_extra_annotated.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "properties": { + "namespace": { + "type": "string", + "x-is-classvar": true, + "default": "test", + "minLength": 1 + } + } +} diff --git a/tests/data/jsonschema/has_classvar_extra_set.json b/tests/data/jsonschema/has_classvar_extra_set.json new file mode 100644 index 000000000..1766e7174 --- /dev/null +++ b/tests/data/jsonschema/has_classvar_extra_set.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "tags": { + "type": "array", + "x-is-classvar": true, + "default": ["a", "b"], + "uniqueItems": true, + "items": { + "type": "string" + } + } + } +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index a76157e8b..4e164bcc4 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -8001,6 +8001,52 @@ def test_validators_requires_pydantic_v2(output_file: Path, tmp_path: Path, caps ) +def test_jsonschema_classvar_extra_pydantic_v2(output_file: Path) -> None: + """Test default value handling.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="has_classvar_extra.py", + extra_args=["--output-model-type", "pydantic_v2.BaseModel", "--field-include-all-keys"], + ) + + +def test_jsonschema_classvar_extra_set_pydantic_v2(output_file: Path) -> None: + """Test ClassVar with set default value.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra_set.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="has_classvar_extra_set.py", + extra_args=[ + "--output-model-type", + "pydantic_v2.BaseModel", + "--field-include-all-keys", + "--use-unique-items-as-set", + ], + ) + + +def test_jsonschema_classvar_extra_annotated_pydantic_v2(output_file: Path) -> None: + """Test ClassVar with use_annotated option.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "has_classvar_extra_annotated.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="has_classvar_extra_annotated.py", + extra_args=[ + "--output-model-type", + "pydantic_v2.BaseModel", + "--field-include-all-keys", + "--use-annotated", + ], + ) + + @PYDANTIC_V2_SKIP def test_unique_items_enum_set(output_file: Path) -> None: """Test set with enum items does not add __hash__ to enum (already hashable)."""