Skip to content

Fix discriminated union simple types#566

Merged
koxudaxi merged 3 commits intomainfrom
issue-480-discriminated-union-simple-types
Apr 30, 2026
Merged

Fix discriminated union simple types#566
koxudaxi merged 3 commits intomainfrom
issue-480-discriminated-union-simple-types

Conversation

@koxudaxi
Copy link
Copy Markdown
Owner

@koxudaxi koxudaxi commented Apr 28, 2026

Fixes: #480

Summary

  • Drop generated Pydantic field discriminators when a oneOf or anyOf union includes non-object variants such as string.
  • Preserve discriminator generation for object-only and non-union discriminator schemas, including object-shaped allOf variants.
  • Guard recursive allOf discriminator inspection against cyclic refs.
  • Add regression fixtures plus runtime import and parser unit checks for mixed simple/model unions.
  • Update generated schema docs, prompt data, and LLM docs fixture counts.

Checks

  • tox -e type
  • tox -e py314-parallel -- tests/test_parser.py::test_get_field_extras_preserves_non_union_discriminator tests/test_parser.py::test_get_field_extras_removes_discriminator_for_all_of_simple_variant tests/test_parser.py::test_get_field_extras_preserves_discriminator_for_all_of_object_variant tests/test_parser.py::test_get_field_extras_removes_discriminator_for_cyclic_all_of_ref tests/main/test_main.py::test_generate_discriminated_union_with_simple_type
  • tox -e schema-docs -- --check
  • tox -e cli-docs -- --check
  • tox -e llms-txt -- --check
  • tox -e fix

Summary by CodeRabbit

  • New Features

    • Improved handling of discriminated unions that include simple (non-object) variants.
    • Generated model support for discriminated-union content types with multiple variant models and a union root.
    • Generated FastAPI app entrypoint for the discriminated-union fixture.
  • Bug Fixes

    • Discriminator metadata is now removed when union variants include non-object types or unresolved combinations.
  • Tests

    • Added tests validating discriminated-union handling and generated module imports.
  • Documentation

    • Updated fixture inventory counts and related docs.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f62b2cd9-f1f1-4a01-a065-4a661e1154ee

📥 Commits

Reviewing files that changed from the base of the PR and between fb6bb11 and 6908089.

📒 Files selected for processing (2)
  • fastapi_code_generator/parser.py
  • tests/test_parser.py

Walkthrough

Overrides field-extras computation to remove generated Pydantic discriminator when an OpenAPI schema includes a discriminator together with oneOf/anyOf that contains at least one non-object-like variant (resolving $ref and traversing allOf). Adds fixture, expected outputs, tests, and docs updates for a discriminated union including a plain string variant.

Changes

Cohort / File(s) Summary
Core Logic
fastapi_code_generator/parser.py
Overrides get_field_extras to drop discriminator when discriminator + oneOf/anyOf contains any non-object-like variant; adds helpers to resolve $ref, evaluate allOf recursively, detect object-like schemas, and avoid ref loops.
OpenAPI Fixture
tests/data/openapi/default_template/discriminated_union_simple_type.yaml
Adds OpenAPI spec: Content is a oneOf of string and four object schemas with a discriminator on kind and mapping entries.
Expected Generated Output
tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py, tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py
Adds expected FastAPI app module and Pydantic models; Content RootModel union includes str and object variants, without a field-level Pydantic discriminator.
Tests
tests/main/test_main.py, tests/test_parser.py
Adds CLI generation test that imports produced models.py and asserts Content exists; unit tests for get_field_extras to verify discriminator preservation/removal across non-union, non-object union, object union, cyclic/ref, and combined oneOf+anyOf cases; introduces test helpers.
Docs / Prompt Data
docs/llms-full.txt, docs/supported_formats.md, fastapi_code_generator/prompt_data.py
Increments fixture counts in docs and prompt preview to include the new discriminated-union fixture (OpenAPI YAML +1, Default template +1).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I found a union, crisp and bright,
String and models sharing light.
I tucked the discriminator out of sight,
So variants hop and take their flight. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 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 objective: fixing generation of discriminated unions when simple types are included, which is the core problem addressed in PR #480.
Linked Issues check ✅ Passed The PR fully addresses issue #480 by removing discriminator generation for unions with non-object variants, preserving it for object-only unions, handling allOf compositions, preventing cyclic refs, and adding comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly related to issue #480: core fix in parser.py, test fixtures/cases, documentation updates for fixture counts, and helper test utilities. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-480-discriminated-union-simple-types

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
Review rate limit: 6/8 reviews remaining, refill in 10 minutes and 53 seconds.

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

📚 Docs Preview: https://pr-566.fastapi-code-generator.pages.dev

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 28, 2026

Merging this PR will not alter performance

⚠️ Unknown Walltime execution environment detected

Using the Walltime instrument on standard Hosted Runners will lead to inconsistent data.

For the most accurate results, we recommend using CodSpeed Macro Runners: bare-metal machines fine-tuned for performance measurement consistency.

✅ 1 untouched benchmark


Comparing issue-480-discriminated-union-simple-types (6908089) with main (7f653a0)

Open in CodSpeed

@koxudaxi koxudaxi force-pushed the issue-480-discriminated-union-simple-types branch 2 times, most recently from 56bc4f6 to 69e8f95 Compare April 28, 2026 03:15
Copy link
Copy Markdown

@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: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fastapi_code_generator/parser.py`:
- Around line 584-598: The current heuristic in _is_object_discriminator_variant
treats any schema with allOf as object-shaped, which misclassifies primitives
wrapped in allOf; update _is_object_discriminator_variant (and by extension how
_has_non_object_discriminator_variant uses it) to inspect allOf contents rather
than assuming object-ness: when schema.allOf is present, recursively examine the
allOf member schemas (resolving refs via
get_ref_model/JsonSchemaObject.model_validate) and only treat the composed
schema as object-shaped if at least one member is object-like (schema.is_object
or schema.properties) or if recursive allOf members themselves contain
object-like subschemas; otherwise do not classify it as an object discriminator
variant. Ensure this logic is applied in _is_object_discriminator_variant and
retained by _has_non_object_discriminator_variant.

In `@tests/main/test_main.py`:
- Around line 230-241: The extra exec() call in
test_generate_discriminated_union_with_simple_type is redundant because
run_cli_and_assert already calls validate_generated_code which compiles the
generated files; remove the
exec(output_dir.joinpath("models.py").read_text(encoding="utf-8"), {}) line, or
if you specifically want to ensure module-level import semantics rather than
simple compilation, replace that exec usage with an explicit module load via
importlib.util.spec_from_file_location / module_from_spec to load the generated
models.py file; update the test to use either no additional check or the
importlib-based load and reference the symbols to ensure imports resolve.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff33acaf-feb2-4eb7-a829-d66f94271dbd

📥 Commits

Reviewing files that changed from the base of the PR and between e9f2965 and 2af2f10.

📒 Files selected for processing (5)
  • fastapi_code_generator/parser.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py
  • tests/data/openapi/default_template/discriminated_union_simple_type.yaml
  • tests/main/test_main.py

Comment thread fastapi_code_generator/parser.py Outdated
Comment thread tests/main/test_main.py Outdated
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (2)
tests/main/test_main.py (1)

241-241: ⚠️ Potential issue | 🟠 Major

Avoid exec in test; it likely fails lint and is unnecessary.

At Line 241, exec(...) triggers Ruff S102. Since this test already goes through run_cli_and_assert(...), drop exec (or switch to explicit importlib loading if you need module-import semantics).

Suggested fix
 def test_generate_discriminated_union_with_simple_type(output_dir: Path) -> None:
     run_cli_and_assert(
         input_path=DATA_PATH
         / OPEN_API_DEFAULT_TEMPLATE_DIR_NAME
         / "discriminated_union_simple_type.yaml",
         output_path=output_dir,
         expected_path=EXPECTED_OPENAPI_PATH
         / "default_template"
         / "discriminated_union_simple_type",
     )
-    exec(output_dir.joinpath("models.py").read_text(encoding="utf-8"), {})
#!/bin/bash
set -euo pipefail

# Verify whether run_cli_and_assert already validates generated code
rg -n -C3 "def run_cli_and_assert|validate_generated_code" tests/main tests/conftest.py tests/main/conftest.py
rg -n -C2 "exec\(" tests/main/test_main.py
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/main/test_main.py` at line 241, The test uses
exec(output_dir.joinpath("models.py").read_text(...), {}) which triggers lint
S102 and is unnecessary because run_cli_and_assert already exercises generated
code; remove the exec call from the test (or replace it with safe import
semantics using importlib.util.spec_from_file_location -> module =
importlib.util.module_from_spec -> spec.loader.exec_module(module) only if you
truly need module import behavior). Update the test to rely on
run_cli_and_assert (and its validation helpers) and reference the exec removal
around the exec(...) invocation and any assertions that depended on it.
fastapi_code_generator/parser.py (1)

595-598: ⚠️ Potential issue | 🟡 Minor

allOf is currently over-classified as object-shaped.

At Line 598, any non-empty allOf returns truthy, so primitive-only allOf branches can be treated as valid discriminator variants. That can leave discriminator attached in mixed unions and reintroduce the runtime Pydantic error this PR fixes.

Suggested fix
 def _is_object_discriminator_variant(self, schema: JsonSchemaObject) -> bool:
     if schema.ref:
         schema = JsonSchemaObject.model_validate(self.get_ref_model(schema.ref))
-    return bool(schema.is_object or schema.properties or schema.allOf)
+    if schema.is_object or schema.properties:
+        return True
+    if not schema.allOf:
+        return False
+    return any(self._is_object_discriminator_variant(member) for member in schema.allOf)
#!/bin/bash
set -euo pipefail

# Verify existing fixture coverage for discriminator + allOf + primitive variants
fd -e yaml -e yml -e json tests/data/openapi | while read -r f; do
  if rg -n "discriminator" "$f" >/dev/null && rg -n "allOf" "$f" >/dev/null; then
    echo "---- $f"
    rg -n -C2 "discriminator|allOf|type:\s*(string|integer|number|boolean|null)" "$f"
  fi
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fastapi_code_generator/parser.py` around lines 595 - 598, The helper
_is_object_discriminator_variant currently treats any non-empty schema.allOf as
object-shaped; change it to inspect allOf's members and return true only if at
least one constituent is object-shaped (i.e., has is_object, properties, or
nested allOf, or a ref that resolves to an object). Update
_is_object_discriminator_variant to, when schema.allOf is present, iterate over
schema.allOf entries, resolve refs via self.get_ref_model and
JsonSchemaObject.model_validate where needed, and only return True if any member
meets the object-like checks; otherwise treat the allOf as non-object-shaped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@fastapi_code_generator/parser.py`:
- Around line 595-598: The helper _is_object_discriminator_variant currently
treats any non-empty schema.allOf as object-shaped; change it to inspect allOf's
members and return true only if at least one constituent is object-shaped (i.e.,
has is_object, properties, or nested allOf, or a ref that resolves to an
object). Update _is_object_discriminator_variant to, when schema.allOf is
present, iterate over schema.allOf entries, resolve refs via self.get_ref_model
and JsonSchemaObject.model_validate where needed, and only return True if any
member meets the object-like checks; otherwise treat the allOf as
non-object-shaped.

In `@tests/main/test_main.py`:
- Line 241: The test uses exec(output_dir.joinpath("models.py").read_text(...),
{}) which triggers lint S102 and is unnecessary because run_cli_and_assert
already exercises generated code; remove the exec call from the test (or replace
it with safe import semantics using importlib.util.spec_from_file_location ->
module = importlib.util.module_from_spec -> spec.loader.exec_module(module) only
if you truly need module import behavior). Update the test to rely on
run_cli_and_assert (and its validation helpers) and reference the exec removal
around the exec(...) invocation and any assertions that depended on it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 784c2f67-bc49-48a4-816a-8ac1f4efc222

📥 Commits

Reviewing files that changed from the base of the PR and between 2af2f10 and 69e8f95.

📒 Files selected for processing (8)
  • docs/llms-full.txt
  • docs/supported_formats.md
  • fastapi_code_generator/parser.py
  • fastapi_code_generator/prompt_data.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py
  • tests/data/openapi/default_template/discriminated_union_simple_type.yaml
  • tests/main/test_main.py
✅ Files skipped from review due to trivial changes (5)
  • docs/supported_formats.md
  • docs/llms-full.txt
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py
  • fastapi_code_generator/prompt_data.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py

@koxudaxi koxudaxi force-pushed the issue-480-discriminated-union-simple-types branch 2 times, most recently from 6e3cdb0 to efe19b0 Compare April 28, 2026 03:21
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (7f653a0) to head (6908089).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #566   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           17        17           
  Lines         1285      1344   +59     
  Branches       131       139    +8     
=========================================
+ Hits          1285      1344   +59     
Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

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.

@koxudaxi koxudaxi force-pushed the issue-480-discriminated-union-simple-types branch 2 times, most recently from 69acf2e to f0c760c Compare April 28, 2026 05:14
Copy link
Copy Markdown

@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: 1

🧹 Nitpick comments (1)
fastapi_code_generator/parser.py (1)

587-593: Consider evaluating both oneOf and anyOf when both are present.

Line 587 currently picks oneOf first (obj.oneOf or obj.anyOf), which can skip anyOf variants if both arrays are set.

♻️ Proposed hardening
-        combined_schemas = obj.oneOf or obj.anyOf
+        combined_schemas = [*(obj.oneOf or []), *(obj.anyOf or [])]
         if not combined_schemas:
             return False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fastapi_code_generator/parser.py` around lines 587 - 593, The current logic
uses combined_schemas = obj.oneOf or obj.anyOf which ignores anyOf when oneOf is
present; change it to aggregate both arrays (e.g., treat missing as empty lists
and concatenate oneOf and anyOf) before checking variants so
_is_object_discriminator_variant runs against all combined schemas; update the
variable combined_schemas and keep the subsequent any(... for schema in
combined_schemas) and reference to _is_object_discriminator_variant unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fastapi_code_generator/parser.py`:
- Around line 595-604: The _is_object_discriminator_variant function can recurse
infinitely through cyclic allOf/$ref chains; modify it to track visited
schemas/refs (e.g., a set of ref strings or ids) and short-circuit when
encountering a previously seen schema to prevent unbounded recursion. Update
_is_object_discriminator_variant (and any internal recursive call) to accept an
optional visited parameter (or use a helper wrapper) and mark the current
schema.ref or id before recursing into get_ref_model() and schema.allOf members,
returning False (or safe default) when a cycle is detected; keep existing logic
for non-cyclic cases and continue using get_ref_model to resolve refs. Ensure
the visited check references unique identifiers (schema.ref or id(schema)) so
cycles are reliably detected.

---

Nitpick comments:
In `@fastapi_code_generator/parser.py`:
- Around line 587-593: The current logic uses combined_schemas = obj.oneOf or
obj.anyOf which ignores anyOf when oneOf is present; change it to aggregate both
arrays (e.g., treat missing as empty lists and concatenate oneOf and anyOf)
before checking variants so _is_object_discriminator_variant runs against all
combined schemas; update the variable combined_schemas and keep the subsequent
any(... for schema in combined_schemas) and reference to
_is_object_discriminator_variant unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 01934ce9-e38b-4a33-90ff-dcd2f8c030e7

📥 Commits

Reviewing files that changed from the base of the PR and between 69e8f95 and 69acf2e.

📒 Files selected for processing (9)
  • docs/llms-full.txt
  • docs/supported_formats.md
  • fastapi_code_generator/parser.py
  • fastapi_code_generator/prompt_data.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py
  • tests/data/openapi/default_template/discriminated_union_simple_type.yaml
  • tests/main/test_main.py
  • tests/test_parser.py
✅ Files skipped from review due to trivial changes (5)
  • docs/supported_formats.md
  • docs/llms-full.txt
  • fastapi_code_generator/prompt_data.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/main.py
  • tests/data/expected/openapi/default_template/discriminated_union_simple_type/models.py

Comment thread fastapi_code_generator/parser.py Outdated
@koxudaxi koxudaxi force-pushed the issue-480-discriminated-union-simple-types branch from f0c760c to 93ca45d Compare April 28, 2026 05:16
@koxudaxi koxudaxi merged commit 71ddd8b into main Apr 30, 2026
45 checks passed
@koxudaxi koxudaxi deleted the issue-480-discriminated-union-simple-types branch April 30, 2026 04:30
@github-actions github-actions Bot added the breaking-change-analyzed PR has been checked for release draft updates label Apr 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Breaking Change Analysis

Result: No breaking changes detected

Reasoning: The breaking-change label is absent, so this PR is treated as a non-breaking release update.


This analysis was performed by repository automation using PR labels and the ## Breaking Changes section.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change-analyzed PR has been checked for release draft updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Discriminated union with simple types (str)

1 participant