diff --git a/docs/cli-reference/model-customization.md b/docs/cli-reference/model-customization.md index 302dac082..3921fec1a 100644 --- a/docs/cli-reference/model-customization.md +++ b/docs/cli-reference/model-customization.md @@ -1125,7 +1125,20 @@ The `--base-class` flag configures the code generation behavior. Specify different base classes for specific models via JSON mapping. The `--base-class-map` option allows you to assign different base classes -to specific models. Priority: base-class-map > customBasePath > base-class. +to specific models. This is useful when you want selective base class inheritance, +for example, applying custom base classes only to specific models while leaving +others with the default `BaseModel`. + +Priority: `--base-class-map` > `customBasePath` (schema extension) > `--base-class` + +You can specify either a single base class as a string, or multiple base classes +(mixins) as a list: + +- Single: `{"Person": "custom.bases.PersonBase"}` +- Multiple: `{"User": ["mixins.AuditMixin", "mixins.TimestampMixin"]}` + +When using multiple base classes, the specified classes are used directly without +adding `BaseModel`. Ensure your mixins inherit from `BaseModel` if needed. **Related:** [`--base-class`](model-customization.md#base-class) diff --git a/docs/jsonschema.md b/docs/jsonschema.md index 0a124c92a..6546a3947 100644 --- a/docs/jsonschema.md +++ b/docs/jsonschema.md @@ -124,9 +124,90 @@ class Defaults(BaseModel): --- +## Custom Base Class with `customBasePath` + +You can specify custom base classes directly in your JSON Schema using the `customBasePath` extension. This allows you to define base classes at the schema level without using CLI options. + +### Single Base Class + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "customBasePath": "myapp.models.UserBase", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"} + }, + "required": ["name", "email"] +} +``` + +**Generated Output:** + +```python +from __future__ import annotations + +from myapp.models import UserBase + + +class User(UserBase): + name: str + email: str +``` + +### Multiple Base Classes (Mixins) + +You can also specify multiple base classes as a list to implement mixin patterns: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "customBasePath": ["mixins.AuditMixin", "mixins.TimestampMixin"], + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"} + }, + "required": ["name", "email"] +} +``` + +**Generated Output:** + +```python +from __future__ import annotations + +from mixins import AuditMixin, TimestampMixin + + +class User(AuditMixin, TimestampMixin): + name: str + email: str +``` + +!!! note "Mixin Usage" + When using multiple base classes, the specified classes are used directly without adding `BaseModel`. + Ensure your mixins inherit from `pydantic.BaseModel` if you need Pydantic model behavior. + +### Priority Resolution + +When multiple base class configurations are present, they are resolved in this order: + +1. **`--base-class-map`** (CLI option) - Highest priority +2. **`customBasePath`** (JSON Schema extension) +3. **`--base-class`** (CLI option) - Lowest priority (default for all models) + +This allows you to set a default base class with `--base-class`, override specific models in the schema with `customBasePath`, and further override at the CLI level with `--base-class-map`. + +--- + ## 📖 See Also - 🖥️ [CLI Reference](cli-reference/index.md) - Complete CLI options reference - 🔧 [CLI Reference: Typing Customization](cli-reference/typing-customization.md) - Type annotation options - 🏷️ [CLI Reference: Field Customization](cli-reference/field-customization.md) - Field naming and constraint options - 📊 [Supported Data Types](supported-data-types.md) - JSON Schema data type support +- 🏗️ [CLI Reference: Model Customization](cli-reference/model-customization.md) - Base class and model customization options diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f457673dc..bdbf5e8e0 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -1988,7 +1988,20 @@ The `--base-class` flag configures the code generation behavior. Specify different base classes for specific models via JSON mapping. The `--base-class-map` option allows you to assign different base classes -to specific models. Priority: base-class-map > customBasePath > base-class. +to specific models. This is useful when you want selective base class inheritance, +for example, applying custom base classes only to specific models while leaving +others with the default `BaseModel`. + +Priority: `--base-class-map` > `customBasePath` (schema extension) > `--base-class` + +You can specify either a single base class as a string, or multiple base classes +(mixins) as a list: + +- Single: `{"Person": "custom.bases.PersonBase"}` +- Multiple: `{"User": ["mixins.AuditMixin", "mixins.TimestampMixin"]}` + +When using multiple base classes, the specified classes are used directly without +adding `BaseModel`. Ensure your mixins inherit from `BaseModel` if needed. **Related:** [`--base-class`](model-customization.md#base-class) @@ -24171,12 +24184,93 @@ class Defaults(BaseModel): --- +## Custom Base Class with `customBasePath` + +You can specify custom base classes directly in your JSON Schema using the `customBasePath` extension. This allows you to define base classes at the schema level without using CLI options. + +### Single Base Class + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "customBasePath": "myapp.models.UserBase", + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"} + }, + "required": ["name", "email"] +} +``` + +**Generated Output:** + +```python +from __future__ import annotations + +from myapp.models import UserBase + + +class User(UserBase): + name: str + email: str +``` + +### Multiple Base Classes (Mixins) + +You can also specify multiple base classes as a list to implement mixin patterns: + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "customBasePath": ["mixins.AuditMixin", "mixins.TimestampMixin"], + "properties": { + "name": {"type": "string"}, + "email": {"type": "string"} + }, + "required": ["name", "email"] +} +``` + +**Generated Output:** + +```python +from __future__ import annotations + +from mixins import AuditMixin, TimestampMixin + + +class User(AuditMixin, TimestampMixin): + name: str + email: str +``` + +!!! note "Mixin Usage" + When using multiple base classes, the specified classes are used directly without adding `BaseModel`. + Ensure your mixins inherit from `pydantic.BaseModel` if you need Pydantic model behavior. + +### Priority Resolution + +When multiple base class configurations are present, they are resolved in this order: + +1. **`--base-class-map`** (CLI option) - Highest priority +2. **`customBasePath`** (JSON Schema extension) +3. **`--base-class`** (CLI option) - Lowest priority (default for all models) + +This allows you to set a default base class with `--base-class`, override specific models in the schema with `customBasePath`, and further override at the CLI level with `--base-class-map`. + +--- + ## 📖 See Also - 🖥️ [CLI Reference](cli-reference/index.md) - Complete CLI options reference - 🔧 [CLI Reference: Typing Customization](cli-reference/typing-customization.md) - Type annotation options - 🏷️ [CLI Reference: Field Customization](cli-reference/field-customization.md) - Field naming and constraint options - 📊 [Supported Data Types](supported-data-types.md) - JSON Schema data type support +- 🏗️ [CLI Reference: Model Customization](cli-reference/model-customization.md) - Base class and model customization options --- diff --git a/src/datamodel_code_generator/__main__.py b/src/datamodel_code_generator/__main__.py index cfc52f236..cfd353323 100644 --- a/src/datamodel_code_generator/__main__.py +++ b/src/datamodel_code_generator/__main__.py @@ -499,7 +499,7 @@ def validate_class_name_affix_scope(cls, v: str | ClassNameAffixScope | None) -> target_python_version: PythonVersion = PythonVersionMin target_pydantic_version: Optional[TargetPydanticVersion] = None # noqa: UP045 base_class: str = "" - base_class_map: Optional[dict[str, str]] = None # noqa: UP045 + base_class_map: Optional[dict[str, str | list[str]]] = None # noqa: UP045 additional_imports: Optional[list[str]] = None # noqa: UP045 class_decorators: Optional[list[str]] = None # noqa: UP045 custom_template_dir: Optional[Path] = None # noqa: UP045 diff --git a/src/datamodel_code_generator/_types/generate_config_dict.py b/src/datamodel_code_generator/_types/generate_config_dict.py index ce0c52847..792113f3f 100644 --- a/src/datamodel_code_generator/_types/generate_config_dict.py +++ b/src/datamodel_code_generator/_types/generate_config_dict.py @@ -46,7 +46,7 @@ class GenerateConfigDict(TypedDict): target_python_version: NotRequired[PythonVersion] target_pydantic_version: NotRequired[TargetPydanticVersion | None] base_class: NotRequired[str] - base_class_map: NotRequired[dict[str, str] | None] + base_class_map: NotRequired[dict[str, str | list[str]] | None] additional_imports: NotRequired[list[str] | None] class_decorators: NotRequired[list[str] | None] custom_template_dir: NotRequired[Path | None] diff --git a/src/datamodel_code_generator/_types/parser_config_dicts.py b/src/datamodel_code_generator/_types/parser_config_dicts.py index f63151bd1..8c2a49eea 100644 --- a/src/datamodel_code_generator/_types/parser_config_dicts.py +++ b/src/datamodel_code_generator/_types/parser_config_dicts.py @@ -46,7 +46,7 @@ class ParserConfigDict(TypedDict): data_type_manager_type: NotRequired[type[DataTypeManager]] data_model_field_type: NotRequired[type[DataModelFieldBase]] base_class: NotRequired[str | None] - base_class_map: NotRequired[dict[str, str] | None] + base_class_map: NotRequired[dict[str, str | list[str]] | None] additional_imports: NotRequired[list[str] | None] class_decorators: NotRequired[list[str] | None] custom_template_dir: NotRequired[Path | None] diff --git a/src/datamodel_code_generator/config.py b/src/datamodel_code_generator/config.py index 18ca37980..ac37fd6dc 100644 --- a/src/datamodel_code_generator/config.py +++ b/src/datamodel_code_generator/config.py @@ -83,7 +83,7 @@ class Config: target_python_version: PythonVersion = PythonVersionMin target_pydantic_version: TargetPydanticVersion | None = None base_class: str = "" - base_class_map: dict[str, str] | None = None + base_class_map: dict[str, str | list[str]] | None = None additional_imports: list[str] | None = None class_decorators: list[str] | None = None custom_template_dir: Path | None = None @@ -225,7 +225,7 @@ class Config: data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField base_class: str | None = None - base_class_map: dict[str, str] | None = None + base_class_map: dict[str, str | list[str]] | None = None additional_imports: list[str] | None = None class_decorators: list[str] | None = None custom_template_dir: Path | None = None diff --git a/src/datamodel_code_generator/model/base.py b/src/datamodel_code_generator/model/base.py index a4a7d9868..193feb5c5 100644 --- a/src/datamodel_code_generator/model/base.py +++ b/src/datamodel_code_generator/model/base.py @@ -643,7 +643,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, @@ -781,14 +781,24 @@ def replace_children_in_models(self, models: list[DataModel], new_ref: Reference child.replace_reference(new_ref) def set_base_class(self) -> None: - """Set up the base class for this model.""" - base_class = self.custom_base_class or self.BASE_CLASS - if not base_class: + """Set up the base class(es) for this model.""" + if self.custom_base_class is None: + base_class_list = [self.BASE_CLASS] if self.BASE_CLASS else [] + elif isinstance(self.custom_base_class, list): + base_class_list = self.custom_base_class + else: + base_class_list = [self.custom_base_class] + + if not base_class_list: self.base_classes = [] return - base_class_import = Import.from_full_path(base_class) - self._additional_imports.append(base_class_import) - self.base_classes = [BaseClassDataType.from_import(base_class_import)] + + result = [] + for base_class in base_class_list: + base_class_import = Import.from_full_path(base_class) + self._additional_imports.append(base_class_import) + result.append(BaseClassDataType.from_import(base_class_import)) + self.base_classes = result @cached_property def template_file_path(self) -> Path: diff --git a/src/datamodel_code_generator/model/dataclass.py b/src/datamodel_code_generator/model/dataclass.py index 70f1645c3..964c6731d 100644 --- a/src/datamodel_code_generator/model/dataclass.py +++ b/src/datamodel_code_generator/model/dataclass.py @@ -58,7 +58,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/enum.py b/src/datamodel_code_generator/model/enum.py index 403ab599b..0f63b1ec5 100644 --- a/src/datamodel_code_generator/model/enum.py +++ b/src/datamodel_code_generator/model/enum.py @@ -51,7 +51,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/msgspec.py b/src/datamodel_code_generator/model/msgspec.py index 9185a2a65..e1903cb38 100644 --- a/src/datamodel_code_generator/model/msgspec.py +++ b/src/datamodel_code_generator/model/msgspec.py @@ -132,7 +132,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/pydantic/base_model.py b/src/datamodel_code_generator/model/pydantic/base_model.py index 70af15e1b..e522660e2 100644 --- a/src/datamodel_code_generator/model/pydantic/base_model.py +++ b/src/datamodel_code_generator/model/pydantic/base_model.py @@ -286,7 +286,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, Any] | None = None, path: Path | None = None, @@ -343,7 +343,7 @@ def __init__( # noqa: PLR0912, PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, Any] | None = None, path: Path | None = None, 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 44e577641..91d5725a2 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/base_model.py +++ b/src/datamodel_code_generator/model/pydantic_v2/base_model.py @@ -245,7 +245,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, Any] | None = None, path: Path | None = None, diff --git a/src/datamodel_code_generator/model/pydantic_v2/dataclass.py b/src/datamodel_code_generator/model/pydantic_v2/dataclass.py index 625f7bf93..1b751adb7 100644 --- a/src/datamodel_code_generator/model/pydantic_v2/dataclass.py +++ b/src/datamodel_code_generator/model/pydantic_v2/dataclass.py @@ -43,7 +43,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/scalar.py b/src/datamodel_code_generator/model/scalar.py index 927088fc7..dcddddb4c 100644 --- a/src/datamodel_code_generator/model/scalar.py +++ b/src/datamodel_code_generator/model/scalar.py @@ -48,7 +48,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/typed_dict.py b/src/datamodel_code_generator/model/typed_dict.py index 6a93dedd9..9e8eded51 100644 --- a/src/datamodel_code_generator/model/typed_dict.py +++ b/src/datamodel_code_generator/model/typed_dict.py @@ -60,7 +60,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/model/union.py b/src/datamodel_code_generator/model/union.py index e24d94144..1375c90fb 100644 --- a/src/datamodel_code_generator/model/union.py +++ b/src/datamodel_code_generator/model/union.py @@ -33,7 +33,7 @@ def __init__( # noqa: PLR0913 fields: list[DataModelFieldBase], decorators: list[str] | None = None, base_classes: list[Reference] | None = None, - custom_base_class: str | None = None, + custom_base_class: str | list[str] | None = None, custom_template_dir: Path | None = None, extra_template_data: defaultdict[str, dict[str, Any]] | None = None, methods: list[str] | None = None, diff --git a/src/datamodel_code_generator/parser/base.py b/src/datamodel_code_generator/parser/base.py index dc1bca6b5..b018c1139 100644 --- a/src/datamodel_code_generator/parser/base.py +++ b/src/datamodel_code_generator/parser/base.py @@ -780,7 +780,7 @@ def __init__( # noqa: PLR0912, PLR0915 self.class_decorators: list[str] = config.class_decorators or [] self.base_class: str | None = config.base_class - self.base_class_map: dict[str, str] | None = config.base_class_map + self.base_class_map: dict[str, str | list[str]] | None = config.base_class_map self.target_python_version: PythonVersion = config.target_python_version self.results: list[DataModel] = [] self.dump_resolve_reference_action: Callable[[Iterable[str]], str] | None = config.dump_resolve_reference_action @@ -1033,11 +1033,27 @@ def _append_additional_imports(self, additional_imports: list[str] | None) -> No new_import = Import.from_full_path(additional_import_string) self.imports.append(new_import) - def _resolve_base_class(self, class_name: str, custom_base_path: str | None = None) -> str | None: - """Resolve base class with priority: base_class_map > customBasePath > base_class.""" + def _resolve_base_class( + self, class_name: str, custom_base_path: str | list[str] | None = None + ) -> str | list[str] | None: + """Resolve base class(es) with priority: base_class_map > customBasePath > base_class.""" + + def normalize(value: str | list[str] | None) -> str | list[str] | None: + if value is None: # pragma: no cover + return None + if isinstance(value, list): + seen: set[str] = set() + result = [v for v in value if isinstance(v, str) and v and v not in seen and not seen.add(v)] # type: ignore[func-returns-value] + if not result: + return None + return result[0] if len(result) == 1 else result + return value or None + if self.base_class_map and class_name in self.base_class_map: - return self.base_class_map[class_name] - return custom_base_path or self.base_class + return normalize(self.base_class_map[class_name]) + if custom_base_path: + return normalize(custom_base_path) + return self.base_class or None def _get_text_from_url(self, url: str) -> str: from datamodel_code_generator.http import DEFAULT_HTTP_TIMEOUT, get_body # noqa: PLC0415 diff --git a/src/datamodel_code_generator/parser/jsonschema.py b/src/datamodel_code_generator/parser/jsonschema.py index c427902dc..c73f23af1 100644 --- a/src/datamodel_code_generator/parser/jsonschema.py +++ b/src/datamodel_code_generator/parser/jsonschema.py @@ -361,7 +361,7 @@ def validate_null_type(cls, value: Any) -> Any: # noqa: N805 default: Any = None id: Optional[str] = Field(default=None, alias="$id") # noqa: UP045 custom_type_path: Optional[str] = Field(default=None, alias="customTypePath") # noqa: UP045 - custom_base_path: Optional[str] = Field(default=None, alias="customBasePath") # noqa: UP045 + custom_base_path: str | list[str] | None = Field(default=None, alias="customBasePath") extras: dict[str, Any] = Field(alias=__extra_key__, default_factory=dict) discriminator: Optional[Union[Discriminator, str]] = None # noqa: UP007, UP045 if is_pydantic_v2(): diff --git a/tests/data/expected/main/input_model/config_class.py b/tests/data/expected/main/input_model/config_class.py index 09fc41050..d8b3b23ae 100644 --- a/tests/data/expected/main/input_model/config_class.py +++ b/tests/data/expected/main/input_model/config_class.py @@ -122,7 +122,7 @@ class GenerateConfig(TypedDict): target_python_version: NotRequired[PythonVersion] target_pydantic_version: NotRequired[TargetPydanticVersion | None] base_class: NotRequired[str] - base_class_map: NotRequired[dict[str, str] | None] + base_class_map: NotRequired[dict[str, str | list[str]] | None] additional_imports: NotRequired[list[str] | None] class_decorators: NotRequired[list[str] | None] custom_template_dir: NotRequired[str | None] diff --git a/tests/data/expected/main/jsonschema/base_class_map_empty_list.py b/tests/data/expected/main/jsonschema/base_class_map_empty_list.py new file mode 100644 index 000000000..ec8c96553 --- /dev/null +++ b/tests/data/expected/main/jsonschema/base_class_map_empty_list.py @@ -0,0 +1,17 @@ +# generated by datamodel-codegen: +# filename: base_class_map_empty_list.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel + + +class Model(BaseModel): + __root__: Any + + +class User(BaseModel): + name: str | None = None diff --git a/tests/data/expected/main/jsonschema/base_class_map_list.py b/tests/data/expected/main/jsonschema/base_class_map_list.py new file mode 100644 index 000000000..f34a91c9b --- /dev/null +++ b/tests/data/expected/main/jsonschema/base_class_map_list.py @@ -0,0 +1,23 @@ +# generated by datamodel-codegen: +# filename: base_class_map_list.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from typing import Any + +from admin import AdminBase +from mixins import AuditMixin, TimestampMixin +from pydantic import BaseModel + + +class Model(BaseModel): + __root__: Any + + +class User(AuditMixin, TimestampMixin): + name: str | None = None + + +class Admin(AdminBase): + role: str | None = None diff --git a/tests/data/expected/main/jsonschema/custom_base_paths_list.py b/tests/data/expected/main/jsonschema/custom_base_paths_list.py new file mode 100644 index 000000000..ab567f124 --- /dev/null +++ b/tests/data/expected/main/jsonschema/custom_base_paths_list.py @@ -0,0 +1,12 @@ +# generated by datamodel-codegen: +# filename: custom_base_paths_list.json +# timestamp: 2019-07-26T00:00:00+00:00 + +from __future__ import annotations + +from mixins import AuditMixin, TimestampMixin + + +class User(AuditMixin, TimestampMixin): + name: str + email: str diff --git a/tests/data/jsonschema/base_class_map_empty_list.json b/tests/data/jsonschema/base_class_map_empty_list.json new file mode 100644 index 000000000..26a1dbd97 --- /dev/null +++ b/tests/data/jsonschema/base_class_map_empty_list.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "User": { + "type": "object", + "properties": { + "name": {"type": "string"} + } + } + } +} diff --git a/tests/data/jsonschema/base_class_map_list.json b/tests/data/jsonschema/base_class_map_list.json new file mode 100644 index 000000000..d592778c6 --- /dev/null +++ b/tests/data/jsonschema/base_class_map_list.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "User": { + "type": "object", + "properties": { + "name": {"type": "string"} + } + }, + "Admin": { + "type": "object", + "properties": { + "role": {"type": "string"} + } + } + } +} diff --git a/tests/data/jsonschema/custom_base_paths_list.json b/tests/data/jsonschema/custom_base_paths_list.json new file mode 100644 index 000000000..65ae172ae --- /dev/null +++ b/tests/data/jsonschema/custom_base_paths_list.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "User", + "type": "object", + "customBasePath": ["mixins.AuditMixin", "mixins.TimestampMixin"], + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "required": ["name", "email"] +} diff --git a/tests/main/jsonschema/test_main_jsonschema.py b/tests/main/jsonschema/test_main_jsonschema.py index 6dda2ee8b..2f1fc14e0 100644 --- a/tests/main/jsonschema/test_main_jsonschema.py +++ b/tests/main/jsonschema/test_main_jsonschema.py @@ -2931,7 +2931,20 @@ def test_main_jsonschema_custom_base_path(output_file: Path) -> None: option_description="""Specify different base classes for specific models via JSON mapping. The `--base-class-map` option allows you to assign different base classes -to specific models. Priority: base-class-map > customBasePath > base-class.""", +to specific models. This is useful when you want selective base class inheritance, +for example, applying custom base classes only to specific models while leaving +others with the default `BaseModel`. + +Priority: `--base-class-map` > `customBasePath` (schema extension) > `--base-class` + +You can specify either a single base class as a string, or multiple base classes +(mixins) as a list: + +- Single: `{"Person": "custom.bases.PersonBase"}` +- Multiple: `{"User": ["mixins.AuditMixin", "mixins.TimestampMixin"]}` + +When using multiple base classes, the specified classes are used directly without +adding `BaseModel`. Ensure your mixins inherit from `BaseModel` if needed.""", input_schema="jsonschema/base_class_map.json", cli_args=[ "--base-class-map", @@ -2959,6 +2972,47 @@ def test_main_jsonschema_base_class_map(output_file: Path) -> None: ) +def test_main_jsonschema_custom_base_paths_list(output_file: Path) -> None: + """Test customBasePath with list of base classes.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "custom_base_paths_list.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="custom_base_paths_list.py", + ) + + +def test_main_jsonschema_base_class_map_list(output_file: Path) -> None: + """Test base_class_map with list values for multiple inheritance.""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "base_class_map_list.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="base_class_map_list.py", + extra_args=[ + "--base-class-map", + '{"User": ["mixins.AuditMixin", "mixins.TimestampMixin"], "Admin": "admin.AdminBase"}', + ], + ) + + +def test_main_jsonschema_base_class_map_empty_list(output_file: Path) -> None: + """Test base_class_map with empty strings list (falls back to default).""" + run_main_and_assert( + input_path=JSON_SCHEMA_DATA_PATH / "base_class_map_empty_list.json", + output_path=output_file, + input_file_type="jsonschema", + assert_func=assert_file_content, + expected_file="base_class_map_empty_list.py", + extra_args=[ + "--base-class-map", + '{"User": ["", ""]}', + ], + ) + + def test_long_description(output_file: Path) -> None: """Test long description handling.""" run_main_and_assert( diff --git a/tests/main/test_public_api_signature_baseline.py b/tests/main/test_public_api_signature_baseline.py index f9fff5b1e..3c0327259 100644 --- a/tests/main/test_public_api_signature_baseline.py +++ b/tests/main/test_public_api_signature_baseline.py @@ -65,7 +65,7 @@ def _baseline_generate( target_python_version: PythonVersion = PythonVersionMin, target_pydantic_version: TargetPydanticVersion | None = None, base_class: str = "", - base_class_map: dict[str, str] | None = None, + base_class_map: dict[str, str | list[str]] | None = None, additional_imports: list[str] | None = None, class_decorators: list[str] | None = None, custom_template_dir: Path | None = None, @@ -201,7 +201,7 @@ def __init__( data_type_manager_type: type[DataTypeManager] = pydantic_model.DataTypeManager, data_model_field_type: type[DataModelFieldBase] = pydantic_model.DataModelField, base_class: str | None = None, - base_class_map: dict[str, str] | None = None, + base_class_map: dict[str, str | list[str]] | None = None, additional_imports: list[str] | None = None, class_decorators: list[str] | None = None, custom_template_dir: Path | None = None,