Skip to content

Fix indefinite hang on OpenAPI schemas with cyclic model dependencies#3078

Draft
kevin-paulson-mindbridge-ai wants to merge 20 commits intokoxudaxi:mainfrom
kevin-paulson-mindbridge-ai:slow_schema_generation_fix
Draft

Fix indefinite hang on OpenAPI schemas with cyclic model dependencies#3078
kevin-paulson-mindbridge-ai wants to merge 20 commits intokoxudaxi:mainfrom
kevin-paulson-mindbridge-ai:slow_schema_generation_fix

Conversation

@kevin-paulson-mindbridge-ai
Copy link
Copy Markdown
Contributor

@kevin-paulson-mindbridge-ai kevin-paulson-mindbridge-ai commented Apr 10, 2026

We've been generating a model with the tool based on our OpenAPI schema, however since version 0.37.0 we've been unsuccessful in doing so. In the latest release (0.56.0) the generation was revealed to be stuck in sort_data_models. I waited up to an hour, I don't think it was ever going to complete. With this change it now works as expected.

I tried to follow https://datamodel-code-generator.koxudaxi.dev/development-contributing/ as best as I can, but let me know if you would like me to make any changes or if I should create an issue or anything else. Thanks!

Summary by CodeRabbit

  • New Features

    • Generate and finalize mutually cyclic models for GraphQL, JSON Schema, and OpenAPI inputs.
  • Bug Fixes

    • Prevent infinite/oscillating ordering when models have circular base-class dependencies during sorting.
    • Skip base-class entries lacking a reference when deriving discriminator mappings to avoid invalid dereferences.
  • Tests

    • Added unit and CLI regression tests for cyclic base-class ordering, discriminator handling, and mutual-cycle model generation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e986b66e-e420-48f9-bd76-9f0ce2377ddb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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
📝 Walkthrough

Walkthrough

Prevented infinite/oscillating ordering in model sorting for circular base-class dependencies and added a guard to skip discriminator base-class slots that lack a Reference. Added unit and integration tests plus generated expected outputs validating cyclic reference handling across GraphQL, JSON Schema, and OpenAPI.

Changes

Cohort / File(s) Summary
Base Parser Logic
src/datamodel_code_generator/parser/base.py
Updated noqa to include PLR0914. In sort_data_models, track seen_orders: set[tuple[str, ...]] and break when an ordering repeats (assigning unresolved_references to the new list) to avoid infinite/oscillating loops. In discriminator-resolution, skip base-classes with falsy reference before calling check_paths.
Parser Tests
tests/parser/test_base.py
Added test_sort_data_models_circular_base_classes_no_infinite_loop and test_apply_discriminator_type_skips_base_class_without_reference to validate stable ordering for circular base-classes and skipping of reference-less base-class slots during discriminator application.
CLI / Integration Tests
tests/main/graphql/test_main_graphql.py, tests/main/jsonschema/test_main_jsonschema.py, tests/main/openapi/test_main_openapi.py
Added regression tests that run the CLI/generator against cyclic schemas (cyclic-mutual-types.graphql, cyclic_mutual_defs.json, cyclic_component_refs.yaml) and assert generated outputs handle mutual/cyclic references.
Generated Expected Outputs
tests/data/expected/main/graphql/cyclic_mutual_types.py, tests/data/expected/main/jsonschema/cyclic_mutual_defs.py, tests/data/expected/main/openapi/cyclic_component_refs.py
Added expected modules showing mutual/cyclic model inheritance relationships (cyclic MRO). Files include appropriate imports/annotations; no public API changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

breaking-change-analyzed

Poem

🐇 I chased a loop that twirled and spun,
I stamped my paw until it was done.
I skip blank slots and stop the race,
Now circular models find their place.
🥕

🚥 Pre-merge checks | ✅ 3
✅ 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 directly describes the main fix: resolving an indefinite hang on OpenAPI schemas with cyclic model dependencies, which aligns with the core change (detecting and breaking out of repeated ordering states in sort_data_models).
Docstring Coverage ✅ Passed Docstring coverage is 92.86% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 10, 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.

✅ 11 untouched benchmarks
⏩ 98 skipped benchmarks1


Comparing kevin-paulson-mindbridge-ai:slow_schema_generation_fix (d10ed4a) with main (e91b601)

Open in CodSpeed

Footnotes

  1. 98 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.

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.

🧹 Nitpick comments (1)
tests/parser/test_base.py (1)

270-303: Consider asserting a postcondition, not just “no exception.”

This catches the crash, but it would be a bit stronger if it also pinned one observable outcome after __apply_discriminator_type() runs, so a future silent no-op doesn’t still pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/parser/test_base.py` around lines 270 - 303, The test currently only
ensures no exception is raised; add a concrete postcondition after calling
parser._Parser__apply_discriminator_type to ensure the union was not silently
modified by the discriminator logic: e.g., assert that union_inner still
contains exactly the two members referencing ref_pet and ref_other (check
identities of union_inner.data_types entries or their .reference attributes) and
that pet_model.base_classes remains unchanged — locate this in
test_apply_discriminator_type_skips_base_class_without_reference and add the
assertion(s) after the call to parser._Parser__apply_discriminator_type([root],
Imports()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/parser/test_base.py`:
- Around line 270-303: The test currently only ensures no exception is raised;
add a concrete postcondition after calling
parser._Parser__apply_discriminator_type to ensure the union was not silently
modified by the discriminator logic: e.g., assert that union_inner still
contains exactly the two members referencing ref_pet and ref_other (check
identities of union_inner.data_types entries or their .reference attributes) and
that pet_model.base_classes remains unchanged — locate this in
test_apply_discriminator_type_skips_base_class_without_reference and add the
assertion(s) after the call to parser._Parser__apply_discriminator_type([root],
Imports()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: af656bb8-01d5-4471-9440-e33695a9c92c

📥 Commits

Reviewing files that changed from the base of the PR and between 05901ff and b4e675c.

📒 Files selected for processing (2)
  • src/datamodel_code_generator/parser/base.py
  • tests/parser/test_base.py

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 86.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.98%. Comparing base (e91b601) to head (d10ed4a).

Files with missing lines Patch % Lines
src/datamodel_code_generator/parser/base.py 81.81% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##              main    #3078      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files           87       87              
  Lines        18306    18319      +13     
  Branches      2091     2094       +3     
===========================================
+ Hits         18306    18317      +11     
- Misses           0        1       +1     
- Partials         0        1       +1     
Flag Coverage Δ
unittests 99.98% <86.66%> (-0.02%) ⬇️

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.

Copy link
Copy Markdown
Collaborator

@ilovelinux ilovelinux left a comment

Choose a reason for hiding this comment

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

This PR implements cycle detection as it is usually done in graph algorithms (BFS, DFS, etc...).

LGTM! Thank you @kevin-paulson-mindbridge-ai 🙂

Copy link
Copy Markdown
Collaborator

@ilovelinux ilovelinux left a comment

Choose a reason for hiding this comment

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

Hi @kevin-paulson-mindbridge-ai, I realized it would be great to add some generation test for schemas with cyclic references in order to track the generated output of this corner-case.

See tests/main/{openapi,jsonschema,graphql}/test_main_*.py for examples. 🙂

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.

🧹 Nitpick comments (2)
tests/main/openapi/test_main_openapi.py (1)

4756-4761: Add an explicit timeout to this hang-regression test.

The test for cyclic component reference handling (which previously caused indefinite hangs) has no timeout protection. If this regression returns, CI will stall until a global job timeout. Since run_main_and_assert() does not enforce a per-test timeout, add one directly to this test using @pytest.mark.timeout() to fast-fail if the code hangs again.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/main/openapi/test_main_openapi.py` around lines 4756 - 4761, This test
calling run_main_and_assert(...) for "cyclic_component_refs.yaml" lacks a
per-test timeout and can hang CI; add a pytest timeout decorator (use
`@pytest.mark.timeout`(seconds)) to the test function that invokes
run_main_and_assert for cyclic_component_refs.yaml so the test fails fast if it
hangs—place the decorator immediately above the test definition and choose a
reasonable timeout (e.g., 10–30s) to prevent indefinite hangs while allowing
normal runs.
tests/main/graphql/test_main_graphql.py (1)

924-929: Make the expected snapshot explicit in this regression test.

On Line 924, this relies on implicit run_main_and_assert behavior for expected output resolution. Please pass expected_file="cyclic_mutual_types.py" explicitly to keep the regression assertion unambiguous and resilient to helper changes.

Proposed patch
 def test_main_cyclic_mutual_types(output_file: Path) -> None:
     """Mutual object-type cycle (A <-> B) in GraphQL."""
     run_main_and_assert(
         input_path=GRAPHQL_DATA_PATH / "cyclic-mutual-types.graphql",
         output_path=output_file,
         input_file_type="graphql",
         assert_func=assert_file_content,
+        expected_file="cyclic_mutual_types.py",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/main/graphql/test_main_graphql.py` around lines 924 - 929, Update the
test call to run_main_and_assert to pass the explicit expected snapshot filename
so the regression is unambiguous: modify the invocation of run_main_and_assert
(the one using input_path=GRAPHQL_DATA_PATH / "cyclic-mutual-types.graphql",
output_path=output_file, input_file_type="graphql",
assert_func=assert_file_content) to include
expected_file="cyclic_mutual_types.py" as an additional argument.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/main/graphql/test_main_graphql.py`:
- Around line 924-929: Update the test call to run_main_and_assert to pass the
explicit expected snapshot filename so the regression is unambiguous: modify the
invocation of run_main_and_assert (the one using input_path=GRAPHQL_DATA_PATH /
"cyclic-mutual-types.graphql", output_path=output_file,
input_file_type="graphql", assert_func=assert_file_content) to include
expected_file="cyclic_mutual_types.py" as an additional argument.

In `@tests/main/openapi/test_main_openapi.py`:
- Around line 4756-4761: This test calling run_main_and_assert(...) for
"cyclic_component_refs.yaml" lacks a per-test timeout and can hang CI; add a
pytest timeout decorator (use `@pytest.mark.timeout`(seconds)) to the test
function that invokes run_main_and_assert for cyclic_component_refs.yaml so the
test fails fast if it hangs—place the decorator immediately above the test
definition and choose a reasonable timeout (e.g., 10–30s) to prevent indefinite
hangs while allowing normal runs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6a41a0a5-bdda-41a2-953a-aae47badee83

📥 Commits

Reviewing files that changed from the base of the PR and between 7eaf6ab and 7786cf9.

⛔ Files ignored due to path filters (3)
  • tests/data/graphql/cyclic-mutual-types.graphql is excluded by !tests/data/**/*.graphql and included by none
  • tests/data/jsonschema/cyclic_mutual_defs.json is excluded by !tests/data/**/*.json and included by none
  • tests/data/openapi/cyclic_component_refs.yaml is excluded by !tests/data/**/*.yaml and included by none
📒 Files selected for processing (6)
  • tests/data/expected/main/graphql/cyclic_mutual_types.py
  • tests/data/expected/main/jsonschema/cyclic_mutual_defs.py
  • tests/data/expected/main/openapi/cyclic_component_refs.py
  • tests/main/graphql/test_main_graphql.py
  • tests/main/jsonschema/test_main_jsonschema.py
  • tests/main/openapi/test_main_openapi.py
✅ Files skipped from review due to trivial changes (3)
  • tests/data/expected/main/openapi/cyclic_component_refs.py
  • tests/data/expected/main/jsonschema/cyclic_mutual_defs.py
  • tests/data/expected/main/graphql/cyclic_mutual_types.py

Copy link
Copy Markdown
Collaborator

@ilovelinux ilovelinux left a comment

Choose a reason for hiding this comment

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

I locally cherry-picked the last two commits (the ones that introduce the generation tests) to main. I expect them to fail (or hang) but they succeed instead.

Given remote:

$ git remote add kevin-paulson-mindbridge-ai https://github.com/kevin-paulson-mindbridge-ai/datamodel-code-generator.git
$ git fetch kevin-paulson-mindbridge-ai

To replicate. exec from main branch:

$ git cherry-pick 7786cf90867db72f716f1796ab071fefcffe4dc5
$ git cherry-pick 8d3ee3958ce8d9c595fdaa3d46a73bba2b54fa23
$ tox -e py314-parallel -- -k cyclic

This means that the cyclic model dependency added as test was already handled by the tool.


In fact, also the other tests (which implements the same cyclic model dependency corner-case) works on main.

$ git switch --detach kevin-paulson-mindbridge-ai/slow_schema_generation_fix
$ git restore src/datamodel_code_generator/parser/base.py --source main
$ tox -e py314-parallel -- -k cyclic

@kevin-paulson-mindbridge-ai could you double-check the corner-case you have in your OpenAPI schema? As now, it looks like the one you proposed was already supported, therefore we cannot reproduce the issue, validate the fix & add regression tests 🙁

…ctually reproduce the issue (don't complete in a reasonable time)
@kevin-paulson-mindbridge-ai
Copy link
Copy Markdown
Contributor Author

[...]

@kevin-paulson-mindbridge-ai could you double-check the corner-case you have in your OpenAPI schema? As now, it looks like the one you proposed was already supported, therefore we cannot reproduce the issue, validate the fix & add regression tests 🙁

My apologies, I neglected to validate that the new tests reproduced the issue before the change. I have updated the 3 tests such that they reproduce the issue before the change (they each ran at least 30 seconds before I killed them) and after the change they pass quickly (less than a second each). They look OK to me, but I am most familiar with OpenAPI and I am very unfamiliar with graphql.

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 `@tests/data/expected/main/jsonschema/cyclic_mutual_defs.py`:
- Around line 14-19: The generated classes Person and Team create an impossible
mutual inheritance cycle (Person(Team, Model) and Team(Person, Model)) causing a
runtime NameError; replace this mutual inheritance with a valid hierarchy by
removing the forward base reference—e.g., have both classes inherit from Model
only or make one inherit from the other (but not both) so that Person and Team
do not form a direct cycle; update the class headers for Person and Team (the
class definitions named Person and Team) accordingly and ensure any
tests/fixtures referencing Model still import/define Model as before.

In `@tests/data/expected/main/openapi/cyclic_component_refs.py`:
- Around line 14-19: The expected fixture currently creates an unimportable
cyclic base-class relationship between Person and Team (Person(Team, Model) and
Team(Person, Model)); fix it by removing the cyclic inheritance and making both
classes inherit only from Model (e.g., class Person(Model): and class
Team(Model):) and, if a cross-reference is needed, add attributes that reference
the other type as a forward annotation (e.g., person has team: "Team" | None =
None and team has member: "Person" | None = None) so the file can be imported
successfully while preserving the relationship between Person and Team.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c31b332b-2832-4806-b2b4-639415011af1

📥 Commits

Reviewing files that changed from the base of the PR and between 8d3ee39 and bb037b0.

⛔ Files ignored due to path filters (3)
  • tests/data/graphql/cyclic-mutual-types.graphql is excluded by !tests/data/**/*.graphql and included by none
  • tests/data/jsonschema/cyclic_mutual_defs.json is excluded by !tests/data/**/*.json and included by none
  • tests/data/openapi/cyclic_component_refs.yaml is excluded by !tests/data/**/*.yaml and included by none
📒 Files selected for processing (3)
  • tests/data/expected/main/graphql/cyclic_mutual_types.py
  • tests/data/expected/main/jsonschema/cyclic_mutual_defs.py
  • tests/data/expected/main/openapi/cyclic_component_refs.py

Comment on lines +14 to +19
class Person(Team, Model):
name: str | None = None


class Team(Person, Model):
name: str | None = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current pattern fails at class creation time (read-only check).
python - <<'PY'
code = """
from __future__ import annotations
from pydantic import BaseModel

class Model(BaseModel):
    pass

class Person(Team, Model):
    name: str | None = None

class Team(Person, Model):
    name: str | None = None
"""
try:
    exec(code, {})
    print("UNEXPECTED: class graph executed successfully")
except Exception as e:
    print(f"Expected failure: {type(e).__name__}: {e}")
PY

Repository: koxudaxi/datamodel-code-generator

Length of output: 132


🏁 Script executed:

# Check the full content of the fixture file
cat -n tests/data/expected/main/jsonschema/cyclic_mutual_defs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 540


🏁 Script executed:

# Look for related test files that might explain the purpose
fd -i cyclic tests/

Repository: koxudaxi/datamodel-code-generator

Length of output: 495


🏁 Script executed:

# Check for input schema files
fd cyclic_mutual tests/ -type f

Repository: koxudaxi/datamodel-code-generator

Length of output: 247


🏁 Script executed:

# Read the input schema file
cat -n tests/data/jsonschema/cyclic_mutual_defs.json

Repository: koxudaxi/datamodel-code-generator

Length of output: 994


🏁 Script executed:

# Search for test code that validates this fixture
rg -l cyclic_mutual_defs tests/ -t py

Repository: koxudaxi/datamodel-code-generator

Length of output: 180


🏁 Script executed:

# Find and read the test file that uses this fixture
rg -B 5 -A 10 cyclic_mutual_defs tests/main/jsonschema/test_main_jsonschema.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 714


🏁 Script executed:

# Check the other cyclic test case (OpenAPI)
cat -n tests/data/expected/main/openapi/cyclic_component_refs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 543


🏁 Script executed:

# Check the GraphQL cyclic test case
cat -n tests/data/expected/main/graphql/cyclic_mutual_types.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 1337


🏁 Script executed:

# Search for any discussion or comments about cyclic references in the codebase
rg -i "cyclic|forward.*ref|mutual.*inherit" tests/ -A 2 -B 2 --max-count=20

Repository: koxudaxi/datamodel-code-generator

Length of output: 18508


🏁 Script executed:

# Search for any TODOs or comments about cyclic inheritance in the codebase
rg -i "cyclic|todo|fixme.*inherit|forward.*ref.*inherit" --max-count=20 datamodel_code_generator/

Repository: koxudaxi/datamodel-code-generator

Length of output: 142


🏁 Script executed:

# Check if there are any comments in the test file about cyclic_mutual_defs
rg -B 10 -A 5 "test_main_cyclic_mutual_defs" tests/main/jsonschema/test_main_jsonschema.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 701


🏁 Script executed:

# Find the source directory
fd -t d "^datamodel" . | head -5

Repository: koxudaxi/datamodel-code-generator

Length of output: 108


🏁 Script executed:

# Search for any issue numbers or comments about cyclic handling
rg -i "cyclic|mutual.*def" . --max-count=30 -t py | grep -E "(TODO|FIXME|issue|#[0-9]+|NOTE)" | head -20

Repository: koxudaxi/datamodel-code-generator

Length of output: 59


🏁 Script executed:

# Search for code related to inheritance and base class generation
rg -i "base.*class|inherit" src/datamodel_code_generator/ -t py --max-count=20 | head -30

Repository: koxudaxi/datamodel-code-generator

Length of output: 3872


🏁 Script executed:

# Check if there's special handling for ordering classes based on dependencies
rg -l "sort|order|depend" src/datamodel_code_generator/ -t py | head -10

Repository: koxudaxi/datamodel-code-generator

Length of output: 526


🏁 Script executed:

# Look for any parser logic that handles allOf or schema references
rg "allOf|definitions" src/datamodel_code_generator/ -t py --max-count=15

Repository: koxudaxi/datamodel-code-generator

Length of output: 8419


🏁 Script executed:

# Examine the SCC (Strongly Connected Components) module for cycle handling
cat src/datamodel_code_generator/parser/_scc.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 5865


🏁 Script executed:

# Search for where allOf is processed and how inheritance is generated
rg "allOf" src/datamodel_code_generator/parser/jsonschema.py -B 3 -A 10 | head -80

Repository: koxudaxi/datamodel-code-generator

Length of output: 3802


🏁 Script executed:

# Look for how base classes are generated from allOf
rg -i "base.*class|inherit" src/datamodel_code_generator/parser/jsonschema.py -B 2 -A 5 | head -100

Repository: koxudaxi/datamodel-code-generator

Length of output: 4456


🏁 Script executed:

# Check the inheritance_forward_ref fixture to see how it handles forward references
cat -n tests/data/expected/main/jsonschema/inheritance_forward_ref.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 1616


🏁 Script executed:

# Also check the input schema for inheritance_forward_ref
cat tests/data/jsonschema/inheritance_forward_ref.json

Repository: koxudaxi/datamodel-code-generator

Length of output: 2386


🏁 Script executed:

# Check if cyclic_mutual_defs.py has model_rebuild calls
tail -20 tests/data/expected/main/jsonschema/cyclic_mutual_defs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 407


🏁 Script executed:

# Look for any comments or documentation about this limitation in the parser code
rg -i "mutual|cyclic.*class|inherit.*cycle" src/datamodel_code_generator/ -B 2 -A 2

Repository: koxudaxi/datamodel-code-generator

Length of output: 1180


🏁 Script executed:

# Check if there's a --keep-model-order option being used and how it affects output
rg "keep.*model.*order|--keep-model-order" tests/data/expected/main/jsonschema/ -l

Repository: koxudaxi/datamodel-code-generator

Length of output: 149


🏁 Script executed:

# Check if there are any GitHub issues or discussions about this in code comments
rg "1466|1467|1468|cyclic.*inherit" src/ tests/ --max-count=10

Repository: koxudaxi/datamodel-code-generator

Length of output: 197


🏁 Script executed:

# Final confirmation: does the current output actually fail to import?
python3 << 'EOF'
import sys
code_path = "tests/data/expected/main/jsonschema/cyclic_mutual_defs.py"
try:
    with open(code_path) as f:
        code = f.read()
    exec(compile(code, code_path, 'exec'), {})
    print("SUCCESS: Code executed without error")
except Exception as e:
    print(f"FAILURE: {type(e).__name__}: {e}")
    sys.exit(1)
EOF

Repository: koxudaxi/datamodel-code-generator

Length of output: 123


Invalid Python output: forward base reference and mutual inheritance cycle

Person(Team, Model) at Line 14 references Team before it exists, and Person/Team form an impossible direct inheritance cycle. The output fails at runtime with NameError: name 'Team' is not defined. Even with from __future__ import annotations, base class names are resolved eagerly at class definition time, so forward references do not help. The model_rebuild() pattern used in other forward-reference fixtures does not apply here because mutual class inheritance cannot be resolved post-hoc.

Consider generating valid output by removing the mutual inheritance:

Proposed fixture correction
-class Person(Team, Model):
+class Person(Model):
     name: str | None = None
 
 
-class Team(Person, Model):
+class Team(Model):
     name: str | None = None
🧰 Tools
🪛 Ruff (0.15.9)

[error] 14-14: Undefined name Team

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/expected/main/jsonschema/cyclic_mutual_defs.py` around lines 14 -
19, The generated classes Person and Team create an impossible mutual
inheritance cycle (Person(Team, Model) and Team(Person, Model)) causing a
runtime NameError; replace this mutual inheritance with a valid hierarchy by
removing the forward base reference—e.g., have both classes inherit from Model
only or make one inherit from the other (but not both) so that Person and Team
do not form a direct cycle; update the class headers for Person and Team (the
class definitions named Person and Team) accordingly and ensure any
tests/fixtures referencing Model still import/define Model as before.

Comment on lines +14 to +19
class Person(Team, Model):
name: str | None = None


class Team(Person, Model):
name: str | None = None
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 13, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
src = """
from __future__ import annotations
class BaseModel: pass
class Model(BaseModel): pass
class Person(Team, Model): pass
class Team(Person, Model): pass
"""
try:
    exec(src, {})
except Exception as exc:
    print(f"{type(exc).__name__}: {exc}")
PY

Repository: koxudaxi/datamodel-code-generator

Length of output: 114


🏁 Script executed:

cat -n tests/data/expected/main/openapi/cyclic_component_refs.py

Repository: koxudaxi/datamodel-code-generator

Length of output: 543


Fix the cyclic inheritance in the expected fixture—this code cannot be imported.

The generated Person and Team classes form an unimportable cycle. Line 14 references Team before it is defined, which raises NameError at class definition time. The from __future__ import annotations pragma only defers annotations, not base-class lookup. Either reject this schema in the generator or adjust the expected output to produce valid Python.

🧰 Tools
🪛 Ruff (0.15.9)

[error] 14-14: Undefined name Team

(F821)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/data/expected/main/openapi/cyclic_component_refs.py` around lines 14 -
19, The expected fixture currently creates an unimportable cyclic base-class
relationship between Person and Team (Person(Team, Model) and Team(Person,
Model)); fix it by removing the cyclic inheritance and making both classes
inherit only from Model (e.g., class Person(Model): and class Team(Model):) and,
if a cross-reference is needed, add attributes that reference the other type as
a forward annotation (e.g., person has team: "Team" | None = None and team has
member: "Person" | None = None) so the file can be imported successfully while
preserving the relationship between Person and Team.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll see if I can create better test data.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@kevin-paulson-mindbridge-ai, sounds good! Here are a few concrete directions you could take:


Option A — Cyclic field references instead of cyclic inheritance

If your input schema uses allOf to pull in referenced types as base classes, consider restructuring it so each type holds the other as an optional field (forward-referenced). This produces importable Python:

# generated by datamodel-codegen:
#   filename:  cyclic_component_refs.yaml
#   timestamp: 2019-07-26T00:00:00+00:00

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel


class Person(BaseModel):
    name: str | None = None
    team: Optional[Team] = None   # forward ref resolved by `from __future__ import annotations`


class Team(BaseModel):
    name: str | None = None
    member: Optional[Person] = None

This is valid Python (the from __future__ import annotations does help for field annotations).


Option B — Non-cyclic inheritance with a shared base

If the test is specifically meant to exercise how the generator handles allOf with a common parent, restructure so the cycle is not in the base-class chain:

class Model(BaseModel):
    pass

class Person(Model):
    name: str | None = None
    team: Team | None = None   # field ref, not inheritance

class Team(Model):
    name: str | None = None

Option C — Adjust the input schema

The root cause is the input YAML/JSON/GraphQL schema itself encoding a pattern (mutual allOf inheritance) that has no valid Python mapping. You could adjust the input schema to use composition via properties instead of allOf-based inheritance, which would make the expected output naturally importable.


Which approach fits your schema best? Happy to help refine the expected fixture once you decide on a direction.


🧠 Learnings used
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:22.111Z
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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@kevin-paulson-mindbridge-ai since you said those examples won't generate any output before1, and now they generate this output, I think this may been introduced by your bugfix.

That's an interesting corner-case. I think we should look into that before merging.

@kevin-paulson-mindbridge-ai IMHO if you can't fix that, consider adding an error to communicate to the user this is an (yet) unsupported corner-case, in order to avoid generating broken code. We may track that in another issue then and fix later. Finding the edges of the problem is the first step to solve it, after all.

CC: @koxudaxi WDYT? 🙂

Footnotes

  1. https://github.com/koxudaxi/datamodel-code-generator/pull/3078#issuecomment-4238086331

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I no longer think that this test data is fully reproducing the original issue and I don't think the fix is quite it either.

What caused me to look at this is we have a somewhat large schema available at test.mindbridge.ai/api/docs/mindbridge-openapi.json which produces with this change an about 20,000 line (valid) python file. Although with this change it will now generate the python file instead of never terminating:

  1. The generated python file produces duplicate union members, this was not noticed as they were hidden by ruff check --fix
  2. I'm having a lot of difficulty determining a test that reproduces the issue and still generates the valid Python expected.

I will plan to continue reviewing this PR later this week to see what I can discover.

@ilovelinux
Copy link
Copy Markdown
Collaborator

ilovelinux commented Apr 13, 2026

My apologies, I neglected to validate that the new tests reproduced the issue before the change.

That's not a problem, I appreciate your effort. Reviews & double-checks are useful because of that 🙂

I have updated the 3 tests such that they reproduce the issue before the change (they each ran at least 30 seconds before I killed them) and after the change they pass quickly (less than a second each). They look OK to me, but I am most familiar with OpenAPI and I am very unfamiliar with graphql.

I'll review it as soon as I'll get some free time 🙂

(they each ran at least 30 seconds before I killed them)

Could you add a timeout as suggested by the first nitpick comment here: #3078 (review)?

@kevin-paulson-mindbridge-ai kevin-paulson-mindbridge-ai marked this pull request as draft April 14, 2026 01:01
@kevin-paulson-mindbridge-ai
Copy link
Copy Markdown
Contributor Author

I think I've got things much closer now. I'll plan to come back to this to:

  1. Add a test for the 1 missed line
  2. Add similar tests for jsonschema and graphql

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.

2 participants