Skip to content

Fix discriminator with allOf without Literal type for Pydantic v2#2722

Merged
koxudaxi merged 2 commits intomainfrom
fix/discriminator-allof-without-literal-type
Dec 21, 2025
Merged

Fix discriminator with allOf without Literal type for Pydantic v2#2722
koxudaxi merged 2 commits intomainfrom
fix/discriminator-allof-without-literal-type

Conversation

@koxudaxi
Copy link
Owner

@koxudaxi koxudaxi commented Dec 21, 2025

Summary by CodeRabbit

  • New Features

    • Improved OpenAPI discriminator handling: short mapping names are normalized and used to build concrete union types, yielding more accurate polymorphic models.
    • Generated models now include explicit discriminator fields (Literal types) where applicable.
  • Tests

    • Added tests and fixtures covering short mapping names, missing mappings, and edge cases with no subtypes.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 21, 2025

Walkthrough

Added helpers to normalize discriminator mapping values to fully-qualified component refs and store normalized discriminators on parsed fields. Enhanced union type resolution to fall back to normalized discriminator mappings when no allOf subtypes are found. Added OpenAPI fixtures and tests for short mapping names and no-mapping scenarios.

Changes

Cohort / File(s) Summary
Discriminator normalization & parser
src/datamodel_code_generator/parser/openapi.py
Added _normalize_discriminator_mapping_ref(mapping_value) and _normalize_discriminator(discriminator) to convert short mapping names to full #/components/schemas/... refs and normalize discriminator dicts. Updated _get_discriminator_union_type() to fall back to normalized mapping values when no allOf subtypes exist. parse_object_fields() now stores the normalized discriminator in field extras.
New/updated OpenAPI fixtures
tests/data/openapi/discriminator_short_mapping_names.yaml
tests/data/openapi/discriminator_no_mapping.yaml
tests/data/openapi/discriminator_no_mapping_no_subtypes.yaml
Added test OpenAPI specs exercising discriminator short mapping names, discriminators without explicit mapping (allOf), and a no-subtypes edge case.
Expected generated models
tests/data/expected/main/openapi/discriminator/short_mapping_names.py
tests/data/expected/main/openapi/discriminator/no_mapping.py
tests/data/expected/main/openapi/discriminator/no_mapping_no_subtypes.py
tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py
Added/updated expected Python model outputs to reflect normalized discriminator mappings and fallback union typing. Concrete models include itemType Literal fields and container fields use union types where appropriate.
Tests
tests/main/openapi/test_main_openapi.py
Added tests: test_main_openapi_discriminator_short_mapping_names(), test_main_openapi_discriminator_no_mapping(), and test_main_openapi_discriminator_no_mapping_no_subtypes() to validate normalization and fallback behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review new normalization helpers for edge cases (values already containing # or /) and consistent ref formatting.
  • Verify _get_discriminator_union_type() fallback correctly derives subtype refs and integrates with existing type resolution.
  • Confirm parse_object_fields() correctly attaches normalized discriminator to field extras and that tests/expected outputs match the new behavior.

Suggested labels

safe-to-fix

Suggested reviewers

  • ilovelinux

Poem

🐰 I nibbled mappings, short and sweet,
From "foo" to a ref that's fully neat;
When allOf hides, I hop and find,
The union types, now well-aligned.
Hooray — the schemas dance complete! 🥕

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: enhancing discriminator handling in OpenAPI parsing, specifically normalizing discriminator mappings and improving union type resolution for cases without allOf subtypes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/discriminator-allof-without-literal-type

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/datamodel_code_generator/parser/openapi.py (2)

398-418: Remove unused noqa directive.

The static analysis tool reports that the noqa: PLR6301 directive on line 398 is unused, as the code doesn't trigger the PLR6301 rule (method could be a static/class method). Since the docstring already explains why this isn't a staticmethod (due to the @snooper_to_methods() decorator), the noqa directive can be safely removed.

🔎 Proposed fix
-    def _normalize_discriminator_mapping_ref(self, mapping_value: str) -> str:  # noqa: PLR6301
+    def _normalize_discriminator_mapping_ref(self, mapping_value: str) -> str:
         """Normalize a discriminator mapping value to a full $ref path.

477-483: Remove unused noqa directive.

The static analysis tool reports that the noqa: PLW2901 directive on line 479 is unused. The code doesn't trigger a loop variable reassignment warning, so this directive can be removed.

🔎 Proposed fix
             if (discriminator := self._discriminator_schemas.get(field.ref))
         ):
             new_field_type = self._get_discriminator_union_type(field.ref) or field_obj.data_type
             normalized_discriminator = self._normalize_discriminator(discriminator)
-            field_obj = self.data_model_field_type(**{  # noqa: PLW2901
+            field_obj = self.data_model_field_type(**{
                 **field_obj.__dict__,
                 "data_type": new_field_type,
                 "extras": {**field_obj.extras, "discriminator": normalized_discriminator},
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d637b0b and 950018a.

📒 Files selected for processing (5)
  • src/datamodel_code_generator/parser/openapi.py (2 hunks)
  • tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py (2 hunks)
  • tests/data/expected/main/openapi/discriminator/short_mapping_names.py (1 hunks)
  • tests/data/openapi/discriminator_short_mapping_names.yaml (1 hunks)
  • tests/main/openapi/test_main_openapi.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/datamodel_code_generator/parser/openapi.py (3)
src/datamodel_code_generator/__main__.py (1)
  • get (112-114)
src/datamodel_code_generator/reference.py (1)
  • get (831-833)
src/datamodel_code_generator/types.py (1)
  • DataType (285-619)
tests/main/openapi/test_main_openapi.py (2)
tests/test_main_kr.py (1)
  • output_file (44-46)
tests/main/conftest.py (2)
  • output_file (94-96)
  • run_main_and_assert (196-352)
tests/data/expected/main/openapi/discriminator/short_mapping_names.py (1)
tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py (4)
  • BaseItem (12-13)
  • FooItem (16-18)
  • BarItem (21-23)
  • ItemContainer (26-27)
🪛 Ruff (0.14.8)
src/datamodel_code_generator/parser/openapi.py

398-398: Unused noqa directive (non-enabled: PLR6301)

Remove unused noqa directive

(RUF100)


479-479: Unused noqa directive (non-enabled: PLW2901)

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). (10)
  • GitHub Check: py312-pydantic1 on Ubuntu
  • GitHub Check: 3.10 on Windows
  • GitHub Check: 3.11 on Windows
  • GitHub Check: 3.12 on macOS
  • GitHub Check: 3.11 on macOS
  • GitHub Check: 3.12 on Windows
  • GitHub Check: 3.14 on Windows
  • GitHub Check: 3.13 on Windows
  • GitHub Check: Analyze (python)
  • GitHub Check: benchmarks
🔇 Additional comments (6)
src/datamodel_code_generator/parser/openapi.py (2)

420-428: LGTM! Clean discriminator normalization.

The method correctly creates a copy of the discriminator dictionary and normalizes all string mapping values to full references. The isinstance(v, str) check appropriately filters mapping values, as per the OpenAPI specification where discriminator mappings should always contain string references.


430-449: Excellent enhancement for discriminator union resolution.

The fallback to discriminator mappings when no allOf subtypes are found elegantly handles the case where schemas define discriminators with explicit mappings but don't use inheritance. The normalization of mapping references ensures compatibility with both short names (FooItem) and full refs (#/components/schemas/FooItem).

This directly addresses the PR objective of fixing discriminators with allOf without Literal type for Pydantic v2.

tests/data/openapi/discriminator_short_mapping_names.yaml (1)

1-50: LGTM! Well-structured test fixture for short mapping names.

This test fixture properly exercises the discriminator normalization functionality by using short mapping names (foo: FooItem, bar: BarItem) instead of full references. The structure with BaseItem, FooItem, BarItem, and ItemContainer provides comprehensive coverage for the discriminator resolution with mapping-based subtypes.

tests/main/openapi/test_main_openapi.py (1)

274-291: LGTM! Comprehensive test for discriminator short mapping names.

The test properly validates the new discriminator normalization functionality with short mapping names. The docstring clearly explains the scenario being tested, and the test follows the established patterns in the test suite. Using pydantic_v2.BaseModel as the output model type is appropriate for testing Pydantic v2 discriminator support.

tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py (1)

7-27: LGTM! Expected output correctly reflects discriminated union pattern.

The generated code properly uses:

  1. Literal types for discriminator fields on FooItem and BarItem
  2. Union type FooItem | BarItem for the ItemContainer.item field
  3. Field(..., discriminator='itemType') to wire up the discriminator

This output demonstrates that the discriminator normalization and union type resolution are working correctly for schemas without allOf inheritance.

tests/data/expected/main/openapi/discriminator/short_mapping_names.py (1)

1-27: LGTM! New expected output correctly validates short mapping name handling.

The expected output properly demonstrates that short discriminator mapping names (foo: FooItem, bar: BarItem) are correctly:

  1. Normalized to full references during parsing
  2. Resolved to create the discriminated union type FooItem | BarItem
  3. Generated with proper Literal discriminator fields and Field(..., discriminator='itemType')

This validates the entire normalization and union resolution pipeline introduced in this PR.

@codecov
Copy link

codecov bot commented Dec 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.35%. Comparing base (ad69407) to head (adb1311).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2722   +/-   ##
=======================================
  Coverage   99.35%   99.35%           
=======================================
  Files          83       83           
  Lines       11722    11744   +22     
  Branches     1412     1416    +4     
=======================================
+ Hits        11646    11668   +22     
  Misses         45       45           
  Partials       31       31           
Flag Coverage Δ
unittests 99.35% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@codspeed-hq
Copy link

codspeed-hq bot commented Dec 21, 2025

CodSpeed Performance Report

Merging #2722 will not alter performance

Comparing fix/discriminator-allof-without-literal-type (adb1311) with main (ad69407)

Summary

✅ 59 untouched
⏩ 10 skipped1

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@koxudaxi koxudaxi force-pushed the fix/discriminator-allof-without-literal-type branch 4 times, most recently from c70ff8c to d79de04 Compare December 21, 2025 03:19
@koxudaxi koxudaxi force-pushed the fix/discriminator-allof-without-literal-type branch from d79de04 to e468f2d Compare December 21, 2025 03:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/datamodel_code_generator/parser/openapi.py (2)

398-418: Remove unused noqa directive.

The # noqa: PLR6301 directive on line 398 is not needed as the rule is not enabled in your configuration.

🔎 Proposed fix
-    def _normalize_discriminator_mapping_ref(self, mapping_value: str) -> str:  # noqa: PLR6301
+    def _normalize_discriminator_mapping_ref(self, mapping_value: str) -> str:

477-481: Remove unused noqa directive.

The # noqa: PLW2901 directive on line 478 is not needed as the rule is not enabled in your configuration.

🔎 Proposed fix
                field_obj = self.data_model_field_type(**{  # noqa: PLW2901
+                field_obj = self.data_model_field_type(**{
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 950018a and adb1311.

📒 Files selected for processing (9)
  • src/datamodel_code_generator/parser/openapi.py (2 hunks)
  • tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py (2 hunks)
  • tests/data/expected/main/openapi/discriminator/no_mapping.py (1 hunks)
  • tests/data/expected/main/openapi/discriminator/no_mapping_no_subtypes.py (1 hunks)
  • tests/data/expected/main/openapi/discriminator/short_mapping_names.py (1 hunks)
  • tests/data/openapi/discriminator_no_mapping.yaml (1 hunks)
  • tests/data/openapi/discriminator_no_mapping_no_subtypes.yaml (1 hunks)
  • tests/data/openapi/discriminator_short_mapping_names.yaml (1 hunks)
  • tests/main/openapi/test_main_openapi.py (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • tests/data/openapi/discriminator_short_mapping_names.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/main/openapi/test_main_openapi.py
  • tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py
🧰 Additional context used
🧬 Code graph analysis (3)
src/datamodel_code_generator/parser/openapi.py (3)
src/datamodel_code_generator/__main__.py (1)
  • get (112-114)
src/datamodel_code_generator/reference.py (1)
  • get (831-833)
src/datamodel_code_generator/types.py (1)
  • DataType (285-619)
tests/data/expected/main/openapi/discriminator/no_mapping.py (1)
tests/data/expected/main/openapi/discriminator/no_mapping_no_subtypes.py (3)
  • BaseItem (10-11)
  • FooItem (14-15)
  • ItemContainer (18-19)
tests/data/expected/main/openapi/discriminator/short_mapping_names.py (3)
tests/data/expected/main/openapi/discriminator/allof_no_subtypes.py (4)
  • BaseItem (12-13)
  • FooItem (16-18)
  • BarItem (21-23)
  • ItemContainer (26-27)
tests/data/expected/main/openapi/discriminator/no_mapping.py (4)
  • BaseItem (12-13)
  • FooItem (16-18)
  • BarItem (21-23)
  • ItemContainer (26-27)
tests/data/expected/main/openapi/discriminator/no_mapping_no_subtypes.py (3)
  • BaseItem (10-11)
  • FooItem (14-15)
  • ItemContainer (18-19)
🪛 Checkov (3.2.334)
tests/data/openapi/discriminator_no_mapping_no_subtypes.yaml

[high] 1-40: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-40: Ensure that security operations is not empty.

(CKV_OPENAPI_5)

tests/data/openapi/discriminator_no_mapping.yaml

[high] 1-49: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1-49: Ensure that security operations is not empty.

(CKV_OPENAPI_5)

🪛 Ruff (0.14.8)
src/datamodel_code_generator/parser/openapi.py

398-398: Unused noqa directive (non-enabled: PLR6301)

Remove unused noqa directive

(RUF100)


478-478: Unused noqa directive (non-enabled: PLW2901)

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). (8)
  • GitHub Check: py312-isort7 on Ubuntu
  • GitHub Check: 3.11 on Windows
  • GitHub Check: 3.14 on Ubuntu
  • GitHub Check: 3.12 on Windows
  • GitHub Check: 3.10 on Windows
  • GitHub Check: 3.11 on macOS
  • GitHub Check: Analyze (python)
  • GitHub Check: benchmarks
🔇 Additional comments (9)
tests/data/openapi/discriminator_no_mapping.yaml (1)

1-47: LGTM! Test fixture is well-structured.

This OpenAPI fixture correctly defines a discriminator scenario without explicit mapping, which tests the parser's ability to infer subtypes from allOf references. The structure is appropriate for testing discriminator normalization logic.

Note: The static analysis security warnings (CKV_OPENAPI_4, CKV_OPENAPI_5) are false positives for test fixtures, as these are not production API specifications.

tests/data/openapi/discriminator_no_mapping_no_subtypes.yaml (1)

1-38: LGTM! Appropriate test fixture for edge case.

This fixture correctly tests the scenario where a discriminator is defined but no allOf subtypes exist. FooItem is defined as a standalone object without extending BaseItem via allOf, which exercises the fallback logic in _get_discriminator_union_type.

Note: The static analysis security warnings are false positives for test fixtures.

tests/data/expected/main/openapi/discriminator/no_mapping_no_subtypes.py (1)

1-19: LGTM! Generated output correctly represents the fixture.

The generated models appropriately reflect the no_mapping_no_subtypes.yaml fixture structure where FooItem is defined independently (not via allOf), and the discriminator is applied to the field in ItemContainer. This is the expected behavior for Pydantic v2 discriminated unions.

src/datamodel_code_generator/parser/openapi.py (4)

398-418: Verify the edge case for dotted filenames without path separators.

The docstring states that bare file references like "other.yaml" will be treated as schema names and require "./other.yaml" format for file references. This could be counterintuitive since .yaml suggests a file. Consider whether this behavior might catch users by surprise.

However, the logic is well-documented and consistent with treating any value without "/" or "#" as a short schema name.

Do you want to add a test case covering the edge case where a mapping value like "Pet.V1" or "other.yaml" is used to ensure the normalization behaves as expected?


420-428: LGTM! Normalization preserves the original discriminator structure.

The method correctly creates a copy of the discriminator dict and only normalizes string mapping values, preserving all other fields intact.


430-448: Excellent fallback logic for discriminators without allOf subtypes.

The enhancement to fall back to discriminator mapping values when no allOf subtypes are found addresses the core issue in the PR. This ensures that discriminators work correctly even when schemas don't explicitly use allOf inheritance but have explicit discriminator mappings.

The logic properly:

  1. Tries allOf subtypes first
  2. Falls back to normalized mapping values if no subtypes exist
  3. Returns None if neither approach yields subtypes

477-481: LGTM! Discriminator normalization correctly applied to field extras.

The change to store the normalized discriminator (via _normalize_discriminator) in field extras ensures that short mapping names are converted to full refs before being used by Pydantic. This is critical for correct discriminated union behavior.

tests/data/expected/main/openapi/discriminator/short_mapping_names.py (1)

1-27: LGTM! Correctly generated discriminated union with short mapping names.

The generated models properly represent a discriminated union where:

  • FooItem and BarItem use Literal types for the discriminator field (itemType)
  • ItemContainer's item field uses a union type with the discriminator attribute
  • The structure aligns with Pydantic v2's discriminated union pattern

This validates that the short mapping name normalization (e.g., "foo""#/components/schemas/FooItem") is working correctly.

tests/data/expected/main/openapi/discriminator/no_mapping.py (1)

1-27: LGTM! Correctly generated discriminated union with inheritance.

The generated models appropriately use inheritance (FooItem and BarItem extend BaseItem) to reflect the allOf structure in the discriminator_no_mapping.yaml fixture. The Literal types use the class names ('FooItem', 'BarItem') as discriminator values, which is the expected behavior when no explicit mapping is provided.

This validates that the discriminator inference from allOf references is working correctly.

@koxudaxi koxudaxi merged commit ddc233b into main Dec 21, 2025
38 checks passed
@koxudaxi koxudaxi deleted the fix/discriminator-allof-without-literal-type branch December 21, 2025 04:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant