Skip to content

Fix RecursionError in _merge_ref_with_schema for circular $ref#2983

Merged
koxudaxi merged 9 commits intomainfrom
fix/circular-ref-merge-recursion-error
Feb 14, 2026
Merged

Fix RecursionError in _merge_ref_with_schema for circular $ref#2983
koxudaxi merged 9 commits intomainfrom
fix/circular-ref-merge-recursion-error

Conversation

@koxudaxi
Copy link
Owner

@koxudaxi koxudaxi commented Feb 7, 2026

Fixes #2971

Summary by CodeRabbit

  • Bug Fixes

    • Prevented infinite recursion when processing circular $ref cases, including those combined with other schema keywords.
  • New Features

    • Better handling of combined $ref + schema constructs to emit correct recursive/self-referential models and forward references.
    • Emits additional model outputs for several circular-ref shapes and an unused-import scenario.
  • Tests

    • Added tests covering circular refs with schema keywords, indirect/root-level cycles, external refs (relative/URL), and the unused-import case.

@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

Walkthrough

Added per-instance circular-$ref detection and caching to the JSON Schema parser; new private helpers traverse resolved $ref targets to detect cycles and short-circuit deep‑merges, deferring emit/parse to reference handling when cycles are found.

Changes

Cohort / File(s) Summary
Parser core
src/datamodel_code_generator/parser/jsonschema.py
Added self._circular_ref_cache and private helpers (_is_ref_circular, _has_ref_cycle, _walk_for_ref, _is_named_schema_definition_path). Integrated circular‑ref checks into _merge_ref_with_schema, parse_combined_schema, and parse_obj to skip or defer deep‑merges when cycles are detected and adjust emit/queue logic for merged attrs containing $ref.
Generated expectations
tests/data/expected/main/jsonschema/circular_ref_with_schema_keywords.py, tests/data/expected/main/jsonschema/circular_ref_indirect.py, tests/data/expected/main/jsonschema/circular_ref_root_with_type.py, tests/data/expected/main/jsonschema/circular_ref_external_relative_keywords.py, tests/data/expected/main/jsonschema/circular_ref_ref_with_schema_keywords.py, tests/data/expected/main/jsonschema/x_python_import_unused.py
Added/updated expected Pydantic model outputs covering multiple circular‑ref patterns and an x‑python‑import unused fixture; includes forward‑ref calls where required.
Tests
tests/main/jsonschema/test_main_jsonschema.py
Added tests for direct and indirect circular $ref cases (including with extra schema keywords), root recursive types, external refs (relative and URL), and x‑python‑import unused behavior.

Sequence Diagram

sequenceDiagram
    participant Parser as JSON Schema Parser
    participant Resolver as Ref Resolver
    participant Cache as Circular Ref Cache
    participant Merger as Schema Merger

    Parser->>Resolver: resolve($ref)
    Resolver-->>Parser: resolved_ref
    Parser->>Cache: _is_ref_circular(resolved_ref)?
    Cache-->>Parser: cached result / miss
    alt cache miss
        Parser->>Parser: _has_ref_cycle(resolved_ref, target, visited)
        Parser->>Parser: _walk_for_ref(schema, target, visited)
        Parser->>Cache: store circular? true/false
    end
    alt Circular Detected
        Parser->>Merger: skip deep-merge → treat as plain $ref
        Merger-->>Parser: unmerged attribute
        Parser->>Parser: parse_ref() / parse_root_type() if named-definition path
    else No Cycle
        Parser->>Merger: deep-merge schemas
        Merger-->>Parser: merged schema
        Parser->>Parser: continue parse_obj flow
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

breaking-change-analyzed

Poem

🐇 I hopped through nested refs and traced each thread,

I cached the loops where tangled paths were spread.
I paused the eager merge and let forward refs be,
Now parsers skip the spiral and models breathe free.
A little hop, and cycles turned to tea.

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 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: fixing a RecursionError in _merge_ref_with_schema for circular $ref, which directly addresses the core issue reported.
Linked Issues check ✅ Passed The PR implements circular reference detection and handling through new private helpers (_is_ref_circular, _has_ref_cycle, _walk_for_ref) and modified logic in _merge_ref_with_schema and parse_obj to prevent infinite recursion when processing circular $ref, directly addressing issue #2971.
Out of Scope Changes check ✅ Passed All changes are within scope: circular ref detection logic, test files for circular reference scenarios, and expected output files for test cases align with fixing the RecursionError issue #2971.
Docstring Coverage ✅ Passed Docstring coverage is 97.22% which is sufficient. The required threshold is 80.00%.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ 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/circular-ref-merge-recursion-error

No actionable comments were generated in the recent review. 🎉


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.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 7, 2026

📚 Docs Preview: https://pr-2983.datamodel-code-generator.pages.dev

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 7, 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 fix/circular-ref-merge-recursion-error (26f1041) with main (9554fb6)

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.

@codecov
Copy link

codecov bot commented Feb 7, 2026

Codecov Report

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

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #2983   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           94        94           
  Lines        17961     18051   +90     
  Branches      2077      2091   +14     
=========================================
+ Hits         17961     18051   +90     
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 fix/circular-ref-merge-recursion-error branch 2 times, most recently from 2adc6be to 4d447f9 Compare February 7, 2026 08:02
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: 1

🤖 Fix all issues with AI agents
In `@src/datamodel_code_generator/parser/jsonschema.py`:
- Around line 1588-1623: The cycle detection fails for refs inside external
files because _has_ref_cycle loads external raw_doc via _get_ref_body(file_part)
but never scopes model_resolver to that file before resolving nested $ref values
in _walk_for_ref; update _has_ref_cycle to enter the resolver context for the
external file (use the ModelResolver context managers current_base_path_context
and/or base_url_context with the external file_part or its derived base) before
calling get_model_by_path/_walk_for_ref so any relative refs are resolved
against the external file, and similarly wrap the resolve_ref call in
_walk_for_ref (or ensure _has_ref_cycle calls _walk_for_ref inside the resolver
context) so resolve_ref uses the correct base when checking visited/resolved
refs; reference symbols: _has_ref_cycle, _walk_for_ref, _get_ref_body,
model_resolver.current_base_path_context, model_resolver.base_url_context,
model_resolver.resolve_ref.

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

🤖 Fix all issues with AI agents
In `@src/datamodel_code_generator/parser/jsonschema.py`:
- Around line 1611-1631: The call to model_resolver.resolve_ref inside
_walk_for_ref can raise custom exceptions beyond KeyError/ValueError and should
be caught so a broken $id doesn't escape the cycle-check; update the try/except
around model_resolver.resolve_ref(ref_value) in _walk_for_ref to catch broader
exceptions (e.g., Exception or the resolver's custom Error) and fall back to
using the raw ref_value (as currently done for KeyError/ValueError), preserving
the existing logic that then compares resolved to target and calls
_has_ref_cycle when appropriate.

@koxudaxi koxudaxi force-pushed the fix/circular-ref-merge-recursion-error branch from 4d447f9 to 13485ab Compare February 7, 2026 09:04
@koxudaxi koxudaxi force-pushed the fix/circular-ref-merge-recursion-error branch from 13485ab to 26f1041 Compare February 7, 2026 09:40
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: 1

🤖 Fix all issues with AI agents
In `@src/datamodel_code_generator/parser/jsonschema.py`:
- Around line 4064-4067: The current early return when encountering obj.ref in
the obj.ref check causes named schemas that are "$ref + keywords" (circular
refs) to skip generating their alias/root model; remove the immediate return
after calling self.parse_ref(obj, path) in the same conditional so parsing still
handles the ref but execution continues to generate the alias/root schema for
the named model (i.e., call self.parse_ref(obj, path) and then proceed to the
subsequent model-generation logic for the current schema instead of returning),
ensuring referenced class names are created; update any tests to cover a schema
with "$ref + keywords" to verify an alias/root model is emitted.

@koxudaxi
Copy link
Owner Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

🤖 Fix all issues with AI agents
In `@src/datamodel_code_generator/parser/jsonschema.py`:
- Around line 1683-1700: _has_ref_cycle currently treats URL-style refs as
filesystem paths causing incorrect context; update _has_ref_cycle to detect URL
refs (use the same is_url check used by resolve_ref) and when file_part is a URL
set base_path to None and root_path to [file_part] (and base_url to file_part)
before entering the model_resolver context managers; keep the existing logic for
non-URL file_part values. This mirrors resolve_ref behavior and ensures cycle
detection for external URL refs uses the correct resolution context (refer to
function _has_ref_cycle and helper _get_ref_body/_walk_for_ref).

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

🤖 Fix all issues with AI agents
In `@src/datamodel_code_generator/parser/jsonschema.py`:
- Around line 1672-1681: The current _is_ref_circular defaults to False on any
exception from _has_ref_cycle which can cause malformed or unreachable refs to
be treated as non-circular and re-processed; change the fallback to True so
unknown/error cases are conservatively treated as circular, cache that result in
self._circular_ref_cache[resolved_ref] and optionally log the exception for
debugging; update the exception branch in _is_ref_circular (which calls
_has_ref_cycle) to set result = True (and store it) so _merge_ref_with_schema
will avoid re-entering problematic refs.

@koxudaxi koxudaxi merged commit b58970a into main Feb 14, 2026
36 of 37 checks passed
@koxudaxi koxudaxi deleted the fix/circular-ref-merge-recursion-error branch February 14, 2026 03:11
@github-actions
Copy link
Contributor

Breaking Change Analysis

Result: No breaking changes detected

Reasoning: This PR is a pure bug fix that resolves RecursionError when processing circular $ref schemas with additional schema keywords. The changes only affect schemas that previously crashed - no existing working schemas have their output changed. All expected test output files in the PR are new test cases, not modifications to existing outputs. The PR enables more JSON Schema patterns to work correctly without changing the behavior for previously working schemas.


This analysis was performed by Claude Code Action

@github-actions
Copy link
Contributor

🎉 Released in 0.54.0

This PR is now available in the latest release. See the release notes for details.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RecursionError: maximum recursion depth exceeded

1 participant

Comments