Add support for multiple base classes in base_class_map and customBasePath#2916
Add support for multiple base classes in base_class_map and customBasePath#2916
Conversation
📝 WalkthroughWalkthroughType annotations and handling for base-class configuration were broadened to accept either a string or a list of strings. Changes touch configuration types, parser resolution logic, model constructors, and tests to support multiple custom base classes and empty-list semantics. Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI / Config
participant Parser as Parser
participant Resolver as _resolve_base_class
participant Model as DataModel.set_base_class
participant Importer as Import collection
CLI->>Parser: provide base_class_map / customBasePath
Parser->>Resolver: resolve base for class_name (may be str|list)
alt base_class_map hit
Resolver-->>Parser: return str | list[str] | None
else custom_base_path present
Resolver-->>Parser: normalize custom_base_path -> str | list[str] | None
else fallback base-class
Resolver-->>Parser: return base_class or None
end
Parser->>Model: pass resolved custom_base_class (str | list)
Model->>Importer: for each base in list -> create Import and BaseClassDataType
Model-->>Parser: attach base_classes and _additional_imports
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (5 passed)
✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (1)
🔇 Additional comments (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
📚 Docs Preview: https://pr-2916.datamodel-code-generator.pages.dev |
CodSpeed Performance ReportMerging #2916 will not alter performanceComparing
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2916 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 93 93
Lines 16865 16888 +23
Branches 1952 1958 +6
=========================================
+ Hits 16865 16888 +23
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/datamodel_code_generator/__main__.py (1)
502-502: Aligns with multi-base support; drop the new# noqa: UP045to satisfy lintThe widened type
Optional[dict[str, str | list[str]]]correctly matches the new multi-base-class semantics used elsewhere. However, Ruff flags the newly added# noqa: UP045here as an unused directive (RUF100); consider removing it on this line while leaving existing legacyUP045suppressions unchanged.tests/main/jsonschema/test_main_jsonschema.py (1)
2988-3000: Clarify “empty list” behavior in docstringThe scenario uses a list of empty strings (
["", ""]) to trigger fallback behavior, whereas the docstring says “empty strings list (falls back to default)” and the function name suggests an “empty list”. Consider tightening the wording to something like “list of empty strings” or, if supported, using an actual empty list ([]) in the JSON to better match the name.src/datamodel_code_generator/parser/base.py (1)
1041-1050: Consider clarifying the deduplication logic.The list comprehension on line 1046 uses a side-effect pattern (
not seen.add(v)) for deduplication that, while correct, is non-obvious:result = [v for v in value if isinstance(v, str) and v and v not in seen and not seen.add(v)]This works because
set.add()returnsNone, makingnot Noneevaluate toTrue. While this is a known Python idiom, it can be confusing to readers.🔎 Alternative implementation for clarity
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] + result: list[str] = [] + for v in value: + if isinstance(v, str) and v and v not in seen: + seen.add(v) + result.append(v) if not result: return None return result[0] if len(result) == 1 else result return value or None
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
tests/data/jsonschema/base_class_map_empty_list.jsonis excluded by!tests/data/**/*.jsonand included by nonetests/data/jsonschema/base_class_map_list.jsonis excluded by!tests/data/**/*.jsonand included by nonetests/data/jsonschema/custom_base_paths_list.jsonis excluded by!tests/data/**/*.jsonand included by none
📒 Files selected for processing (22)
src/datamodel_code_generator/__main__.pysrc/datamodel_code_generator/_types/generate_config_dict.pysrc/datamodel_code_generator/_types/parser_config_dicts.pysrc/datamodel_code_generator/config.pysrc/datamodel_code_generator/model/base.pysrc/datamodel_code_generator/model/dataclass.pysrc/datamodel_code_generator/model/enum.pysrc/datamodel_code_generator/model/msgspec.pysrc/datamodel_code_generator/model/pydantic/base_model.pysrc/datamodel_code_generator/model/pydantic_v2/base_model.pysrc/datamodel_code_generator/model/pydantic_v2/dataclass.pysrc/datamodel_code_generator/model/scalar.pysrc/datamodel_code_generator/model/typed_dict.pysrc/datamodel_code_generator/model/union.pysrc/datamodel_code_generator/parser/base.pysrc/datamodel_code_generator/parser/jsonschema.pytests/data/expected/main/input_model/config_class.pytests/data/expected/main/jsonschema/base_class_map_empty_list.pytests/data/expected/main/jsonschema/base_class_map_list.pytests/data/expected/main/jsonschema/custom_base_paths_list.pytests/main/jsonschema/test_main_jsonschema.pytests/main/test_public_api_signature_baseline.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-01-02T08:25:19.839Z
Learnt from: koxudaxi
Repo: koxudaxi/datamodel-code-generator PR: 2890
File: tests/data/expected/main/jsonschema/ref_nullable_with_constraint.py:14-15
Timestamp: 2026-01-02T08:25:19.839Z
Learning: The datamodel-code-generator currently generates RootModel subclasses with an explicit `root` field annotation (e.g., `class StringType(RootModel[str]): root: str`). This is existing behavior of the code generator and should not be flagged as an issue introduced by new changes.
Applied to files:
src/datamodel_code_generator/model/scalar.pysrc/datamodel_code_generator/__main__.pytests/data/expected/main/jsonschema/base_class_map_empty_list.py
🧬 Code graph analysis (4)
tests/data/expected/main/jsonschema/custom_base_paths_list.py (3)
tests/data/expected/main/jsonschema/base_class_map_empty_list.py (1)
User(16-17)tests/data/expected/main/jsonschema/base_class_map_list.py (1)
User(18-19)src/datamodel_code_generator/model/base.py (1)
name(838-840)
src/datamodel_code_generator/model/base.py (2)
src/datamodel_code_generator/imports.py (3)
Import(20-38)from_full_path(35-38)append(74-89)src/datamodel_code_generator/types.py (1)
from_import(496-520)
tests/data/expected/main/jsonschema/base_class_map_list.py (2)
tests/data/expected/main/jsonschema/base_class_map_empty_list.py (2)
Model(12-13)User(16-17)tests/data/expected/main/jsonschema/custom_base_paths_list.py (1)
User(10-12)
tests/data/expected/main/jsonschema/base_class_map_empty_list.py (1)
tests/data/expected/main/jsonschema/base_class_map_list.py (2)
Model(14-15)User(18-19)
🪛 Ruff (0.14.10)
src/datamodel_code_generator/__main__.py
502-502: Unused noqa directive (non-enabled: UP045)
Remove unused noqa directive
(RUF100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
- GitHub Check: py312-black24 on Ubuntu
- GitHub Check: py312-isort6 on Ubuntu
- GitHub Check: 3.10 on Windows
- GitHub Check: py312-isort7 on Ubuntu
- GitHub Check: 3.11 on Windows
- GitHub Check: py312-pydantic1 on Ubuntu
- GitHub Check: 3.13 on Windows
- GitHub Check: 3.14 on Windows
- GitHub Check: 3.10 on macOS
- GitHub Check: py312-isort5 on Ubuntu
- GitHub Check: py312-black23 on Ubuntu
- GitHub Check: py312-black22 on Ubuntu
- GitHub Check: 3.13 on Ubuntu
- GitHub Check: 3.11 on macOS
- GitHub Check: 3.11 on Ubuntu
- GitHub Check: 3.12 on Windows
- GitHub Check: 3.10 on Ubuntu
- GitHub Check: benchmarks
- GitHub Check: Analyze (python)
🔇 Additional comments (25)
src/datamodel_code_generator/model/msgspec.py (1)
135-162: LGTM! Consistent type widening for multiple base class support.The parameter type has been appropriately widened to
str | list[str] | Noneand passed through to the parent class, consistent with the broader pattern across model constructors.src/datamodel_code_generator/model/pydantic/base_model.py (2)
289-317: LGTM! BaseModelBase type widening aligns with model base classes.The
custom_base_classparameter type has been correctly widened to support multiple base classes.
346-371: LGTM! BaseModel type widening aligns with parent class.The
custom_base_classparameter type has been correctly widened and passed through toBaseModelBase.__init__().src/datamodel_code_generator/model/pydantic_v2/base_model.py (1)
248-273: LGTM! Pydantic v2 BaseModel type widening aligns with v1.The
custom_base_classparameter type has been correctly widened to support multiple base classes, consistent with the Pydantic v1 implementation.src/datamodel_code_generator/model/pydantic_v2/dataclass.py (1)
46-76: LGTM! DataClass type widening completes the consistent pattern.The
custom_base_classparameter type has been correctly widened tostr | list[str] | None, completing the consistent update across all model constructors.src/datamodel_code_generator/model/typed_dict.py (1)
63-90: The parentDataModelclass properly handles thestr | list[str]type forcustom_base_classthrough theset_base_class()method, which usesisinstance()checks to normalize both single strings and lists of base class names into the internalbase_class_list. TypedDict correctly delegates to this parent implementation without requiring any additional handling.src/datamodel_code_generator/model/enum.py (1)
54-54: Type widening aligns with parent DataModel.The
custom_base_classparameter type has been correctly widened to support multiple base classes. This change is consistent with the broader PR objective and relies on the parentDataModelclass to handle list values appropriately.tests/data/expected/main/input_model/config_class.py (1)
125-125: Expected test output correctly reflects the type widening.This generated test expectation file properly reflects the broadened
base_class_maptype to support multiple base classes viastr | list[str].src/datamodel_code_generator/_types/parser_config_dicts.py (1)
49-49: Type change consistent with GenerateConfigDict.The
base_class_maptype widening inParserConfigDictaligns with the corresponding change inGenerateConfigDict. The parser implementation must handle both string and list values appropriately.Verification of the parser's base_class_map handling logic has already been requested for the
GenerateConfigDictfile. The same verification applies here to ensure the parser correctly processes both single and multiple base class specifications.src/datamodel_code_generator/model/dataclass.py (1)
61-61: No action needed—parent DataModel class properly handles list[str] for custom_base_class.The parent DataModel class in
src/datamodel_code_generator/model/base.pyalready acceptsstr | list[str] | Noneat line 646 and correctly implements support for multiple base classes. Theset_base_class()method (lines 787–790) explicitly handles both string and list cases: lists are used directly, and strings are wrapped in a list before being assigned tobase_classes.src/datamodel_code_generator/_types/generate_config_dict.py (1)
49-49: Implementation already properly handles base_class_map resolution for strings, lists, and edge cases.The
_resolve_base_class()method insrc/datamodel_code_generator/parser/base.py(lines 1036-1056) includes comprehensive logic via itsnormalize()helper function that:
- Handles both single strings and lists of strings correctly
- Treats empty lists as no base class (filters empty strings and returns None if all items are removed)
- Maintains proper inheritance order (single-item lists collapse to strings, multi-item lists remain as lists)
Resolution priority is documented: base_class_map > customBasePath > base_class.
tests/data/expected/main/jsonschema/custom_base_paths_list.py (1)
1-12: Fixture correctly exercises list-valuedcustomBasePath
User(AuditMixin, TimestampMixin)cleanly reflects a schema withcustomBasePathas a list; fields and imports look consistent with the rest of the expected outputs.src/datamodel_code_generator/config.py (1)
86-86: Config base_class_map types now consistently support multiple basesUsing
dict[str, str | list[str]] | Nonefor bothGenerateConfig.base_class_mapandParserConfig.base_class_mapkeeps the public config models aligned with the new multi-inheritance behavior and matches how aliases already allowstr | list[str]values.Also applies to: 228-228
src/datamodel_code_generator/parser/jsonschema.py (1)
364-364: JsonSchemaObject.custom_base_path correctly widened for multiple custom basesAllowing
custom_base_path: str | list[str] | Nonematches how this field is only forwarded into_resolve_base_class, and enablescustomBasePathto be a list in schemas while remaining backward compatible with string values.tests/data/expected/main/jsonschema/base_class_map_list.py (1)
1-23: Expected output for list-valued base_class_map is consistentThe generated
Model,User(AuditMixin, TimestampMixin), andAdmin(AdminBase)classes align with abase_class_mapthat assigns multiple bases toUser, a single custom base toAdmin, and leaves the rootModelonBaseModel. This fixture looks correct and self-consistent.tests/data/expected/main/jsonschema/base_class_map_empty_list.py (1)
1-17: LGTM! Test fixture correctly demonstrates empty list behavior.This test fixture appropriately shows that when
base_class_mapcontains an empty list for theUsermodel, it inherits only from the defaultBaseModelwithout additional mixins. This contrasts correctly withbase_class_map_list.pywhereUserinherits from multiple base classes.src/datamodel_code_generator/model/scalar.py (1)
51-51: Type broadening is consistent with union model changes.The
custom_base_classparameter type widening tostr | list[str] | Nonemirrors the identical change inunion.pyand enables scalar type models to inherit from multiple custom base classes. The parameter is correctly passed through to the parentDataModelclass.tests/main/test_public_api_signature_baseline.py (1)
68-68: Type signature updates are correct and properly implemented for multiple base class support.The baseline signatures at lines 68 and 204 correctly match the actual implementation in
config.pyandbase.py. The widening ofbase_class_mapfromdict[str, str]todict[str, str | list[str]]is backward-compatible and fully supported by the_resolve_base_class()method, which properly:
- Normalizes both
strandlist[str]values- Deduplicates entries in lists and removes empty strings
- Applies priority:
base_class_map>custom_base_path>base_class- Returns a single string if a list contains only one element, otherwise returns the list
src/datamodel_code_generator/model/union.py (1)
36-36: Parent signature already supports list[str] - implementation properly handles both str and list[str] for custom base classes.The
custom_base_classparameter signature matches the parentDataModelclass, which already supportsstr | list[str] | None. Theset_base_class()method correctly handles both forms: wrapping a single string in a list, and passing through an existing list directly. Both are converted toBaseClassDataTypeobjects for template rendering.tests/main/jsonschema/test_main_jsonschema.py (2)
2962-2970: Good coverage forcustomBasePathlist semanticsThis mirrors the existing single-base
custom_base_pathtest and cleanly covers the list variant using fixtures, consistent with the rest of the suite.
2973-2985: Solid regression test for list-valuedbase_class_mapThis test exercises mixed usage (list for
User, string forAdmin) via--base-class-map, which is exactly what’s needed to validate multiple inheritance support and backward compatibility.src/datamodel_code_generator/model/base.py (2)
646-646: LGTM: Type annotation updated to support multiple base classes.The signature change to accept
str | list[str] | Noneproperly enables the multiple inheritance feature requested in issues #1918 and #2494.
783-801: LGTM: Clean refactor to support multiple base classes.The logic correctly handles all three input forms (None, string, list) and properly creates imports and base class data types for each. The early return on empty list is a good optimization.
src/datamodel_code_generator/parser/base.py (2)
783-783: LGTM: Type annotation updated for base_class_map.The type change to
dict[str, str | list[str]] | Nonealigns with the broader PR objective to support multiple base classes per model in the mapping.
1052-1056: LGTM: Resolution priority is clear and correctly implemented.The priority order (base_class_map > custom_base_path > base_class) aligns with the docstring and provides proper fallback behavior. The normalization is consistently applied to inputs that may be lists.
Generated by GitHub Actions
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
docs/cli-reference/model-customization.md (1)
1123-1187: Missing expected output in--base-class-mapexample section.The examples section (lines 1153-1186) provides an Input Schema but shows an error message instead of actual generated code. Line 1185 displays
> **Error:** File not found: base_class_map.pywhere the Python output should be.Provide the expected generated code output demonstrating how multiple and/or single base classes are applied to the
Person,Animal, andCarmodels based on the--base-class-mapconfiguration.🔎 Expected fix: Add proper example output
Replace lines 1183-1186 with actual generated Python code showing the three models (Person, Animal, Car) inheriting from their specified base classes. For example:
# generated by datamodel-codegen: # filename: schema.json # timestamp: 2019-07-26T00:00:00+00:00 from __future__ import annotations from custom.bases import PersonBase, AnimalBase from pydantic import BaseModel class Person(PersonBase): name: str class Animal(AnimalBase): species: str class Car(BaseModel): model: strAlternatively, if the intent is to show multiple base classes in the output, adjust both the command and example accordingly to demonstrate that feature.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by none and included by none
📒 Files selected for processing (3)
docs/cli-reference/model-customization.mddocs/jsonschema.mdtests/main/jsonschema/test_main_jsonschema.py
🚧 Files skipped from review as they are similar to previous changes (1)
- tests/main/jsonschema/test_main_jsonschema.py
🔇 Additional comments (1)
docs/jsonschema.md (1)
127-205: Excellent documentation forcustomBasePathfeature.The new section provides clear, comprehensive coverage of the
customBasePathextension for JSON Schema:
- Well-structured with separate examples for single and multiple base classes
- Accurate Python code output demonstrating correct import statements and inheritance syntax
- Helpful note about mixin behavior and BaseModel requirements (lines 191–193)
- Clear priority resolution explanation aligning with the broader base-class system
The examples are realistic and demonstrate both common use cases (single inheritance and mixin composition).
Breaking Change AnalysisResult: No breaking changes detected Reasoning: This PR is backwards compatible. The changes extend existing functionality without breaking existing usage:
All existing configurations, API calls, and CLI usage patterns will produce the same output as before. The new list syntax is purely additive functionality. This analysis was performed by Claude Code Action |
|
🎉 Released in 0.52.2 This PR is now available in the latest release. See the release notes for details. |
Fixes: #1918
Fixes: #2494
Summary by CodeRabbit
New Features
Tests
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.