From 637a1bd0dddff0573f246a068afccb6867a2d1aa Mon Sep 17 00:00:00 2001 From: ubaumann Date: Mon, 5 Jan 2026 02:52:38 +0100 Subject: [PATCH 1/6] Support ClassVar for Pydantic v2 --- .../model/pydantic_v2/base_model.py | 11 +++++++++++ .../model/template/pydantic_v2/BaseModel.jinja2 | 4 +++- .../expected/main/jsonschema/has_classvar_extra.py | 13 +++++++++++++ tests/data/jsonschema/has_classvar_extra.json | 10 ++++++++++ tests/main/jsonschema/test_main_jsonschema.py | 12 ++++++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/data/expected/main/jsonschema/has_classvar_extra.py create mode 100644 tests/data/jsonschema/has_classvar_extra.json 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..05176a316 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -31,6 +31,7 @@ IMPORT_VALIDATION_INFO, IMPORT_VALIDATOR_FUNCTION_WRAP_HANDLER, ) +from datamodel_code_generator.model.imports import IMPORT_CLASSVAR from datamodel_code_generator.reference import ModelResolver from datamodel_code_generator.types import chain_as_tuple from datamodel_code_generator.util import field_validator, model_validate, model_validator @@ -187,11 +188,21 @@ def _has_discriminator_in_data_type(self) -> bool: """Check if any nested DataType has a discriminator.""" return any(dt.discriminator for dt in self.data_type.all_data_types) + @property + def class_var_type_hint(self) -> str: + return f"ClassVar[{self.type_hint}]" + + @property + def is_class_var(self) -> bool: + return self.extras.get("x-is_classvar") is True + @property 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/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 index 828c4b416..b4d938af0 100644 --- a/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +++ b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 @@ -19,7 +19,9 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme {{ line }} {%- endfor %} {%- for field in fields %} - {%- if not field.annotated and field.field %} + {%- if field.is_class_var and field.represented_default %} + {{ field.name }}: {{ field.class_var_type_hint }} = {{ field.represented_default }} + {%- elif not field.annotated and field.field %} {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} {%- if field.annotated %} 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..de0dcf550 --- /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, Field + + +class Model(BaseModel): + namespace: ClassVar[str | None] = 'test' diff --git a/tests/data/jsonschema/has_classvar_extra.json b/tests/data/jsonschema/has_classvar_extra.json new file mode 100644 index 000000000..e0fc1da56 --- /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/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index a76157e8b..c37521198 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -8001,6 +8001,18 @@ 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"], + ) + + @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).""" From 029e2cd8f262fce46dd2d065a3c8d0eb3c082f5a Mon Sep 17 00:00:00 2001 From: ubaumann Date: Tue, 6 Jan 2026 04:09:22 +0100 Subject: [PATCH 2/6] Implement ClassVar feature in code without changing template --- .../model/pydantic/base_model.py | 22 ++++++++++++++++--- .../model/pydantic_v2/base_model.py | 8 ------- .../template/pydantic_v2/BaseModel.jinja2 | 4 +--- tests/data/jsonschema/has_classvar_extra.json | 2 +- 4 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 44a483ec8..befe29077 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, @@ -250,18 +250,34 @@ 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: + 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: + 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.name == "name": + # breakpoint() + 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): return None + if self.is_class_var: + return self.type_hint return f"Annotated[{self.type_hint}, {self!s}]" @property 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 05176a316..340360136 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -188,14 +188,6 @@ def _has_discriminator_in_data_type(self) -> bool: """Check if any nested DataType has a discriminator.""" return any(dt.discriminator for dt in self.data_type.all_data_types) - @property - def class_var_type_hint(self) -> str: - return f"ClassVar[{self.type_hint}]" - - @property - def is_class_var(self) -> bool: - return self.extras.get("x-is_classvar") is True - @property def imports(self) -> tuple[Import, ...]: """Get all required imports including AliasChoices and Field for discriminator.""" diff --git a/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 index b4d938af0..828c4b416 100644 --- a/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 +++ b/src/datamodel_code_generator/model/template/pydantic_v2/BaseModel.jinja2 @@ -19,9 +19,7 @@ class {{ class_name }}({{ base_class }}):{% if comment is defined %} # {{ comme {{ line }} {%- endfor %} {%- for field in fields %} - {%- if field.is_class_var and field.represented_default %} - {{ field.name }}: {{ field.class_var_type_hint }} = {{ field.represented_default }} - {%- elif not field.annotated and field.field %} + {%- if not field.annotated and field.field %} {{ field.name }}: {{ field.type_hint }} = {{ field.field }} {%- else %} {%- if field.annotated %} diff --git a/tests/data/jsonschema/has_classvar_extra.json b/tests/data/jsonschema/has_classvar_extra.json index e0fc1da56..7c27e0e16 100644 --- a/tests/data/jsonschema/has_classvar_extra.json +++ b/tests/data/jsonschema/has_classvar_extra.json @@ -3,7 +3,7 @@ "properties": { "namespace": { "type": "string", - "x-is_classvar": true, + "x-is-classvar": true, "default": "test" } } From 07dd075819b192f35e2232cadb08ea24f585af44 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 07:43:26 +0000 Subject: [PATCH 3/6] Remove commented-out debug code --- src/datamodel_code_generator/model/pydantic/base_model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index befe29077..7b1d297df 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -260,13 +260,12 @@ def __str__(self) -> str: # noqa: PLR0912 @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.name == "name": - # breakpoint() if self.is_class_var: return f"ClassVar[{super().type_hint}]" return super().type_hint From bb0934612251b313fe46be81a4b5144d6a582d81 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 08:30:24 +0000 Subject: [PATCH 4/6] Fix unused Field import for ClassVar fields --- src/datamodel_code_generator/model/pydantic/base_model.py | 2 ++ tests/data/expected/main/jsonschema/has_classvar_extra.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 7b1d297df..1d650d587 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -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 diff --git a/tests/data/expected/main/jsonschema/has_classvar_extra.py b/tests/data/expected/main/jsonschema/has_classvar_extra.py index de0dcf550..fc8129ea3 100644 --- a/tests/data/expected/main/jsonschema/has_classvar_extra.py +++ b/tests/data/expected/main/jsonschema/has_classvar_extra.py @@ -6,7 +6,7 @@ from typing import ClassVar -from pydantic import BaseModel, Field +from pydantic import BaseModel class Model(BaseModel): From 7234d267aff0024165f3a0b3c8fa70cb4aef2f24 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 08:40:21 +0000 Subject: [PATCH 5/6] Add UNDEFINED check for ClassVar default and additional tests --- .../model/pydantic/base_model.py | 6 ++-- .../has_classvar_extra_annotated.py | 13 +++++++ .../main/jsonschema/has_classvar_extra_set.py | 13 +++++++ .../has_classvar_extra_annotated.json | 11 ++++++ .../jsonschema/has_classvar_extra_set.json | 14 ++++++++ tests/main/jsonschema/test_main_jsonschema.py | 34 +++++++++++++++++++ 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 tests/data/expected/main/jsonschema/has_classvar_extra_annotated.py create mode 100644 tests/data/expected/main/jsonschema/has_classvar_extra_set.py create mode 100644 tests/data/jsonschema/has_classvar_extra_annotated.json create mode 100644 tests/data/jsonschema/has_classvar_extra_set.json diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 1d650d587..6368a9379 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -256,6 +256,8 @@ def __str__(self) -> str: # noqa: PLR0912 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)})" @@ -275,10 +277,8 @@ def type_hint(self) -> str: @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 - if self.is_class_var: - return self.type_hint return f"Annotated[{self.type_hint}, {self!s}]" @property 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_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 c37521198..4e164bcc4 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -8013,6 +8013,40 @@ def test_jsonschema_classvar_extra_pydantic_v2(output_file: Path) -> None: ) +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).""" From bdee85844eaa2c6b51098859011a03c2ca2c3a72 Mon Sep 17 00:00:00 2001 From: Koudai Aono Date: Tue, 6 Jan 2026 14:13:28 +0000 Subject: [PATCH 6/6] Fix import order --- src/datamodel_code_generator/model/pydantic_v2/base_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 340360136..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, ) @@ -31,7 +32,6 @@ IMPORT_VALIDATION_INFO, IMPORT_VALIDATOR_FUNCTION_WRAP_HANDLER, ) -from datamodel_code_generator.model.imports import IMPORT_CLASSVAR from datamodel_code_generator.reference import ModelResolver from datamodel_code_generator.types import chain_as_tuple from datamodel_code_generator.util import field_validator, model_validate, model_validator