fix(cli): gracefully handle circular $ref errors in OpenAPI specs#15064
fix(cli): gracefully handle circular $ref errors in OpenAPI specs#15064devin-ai-integration[bot] wants to merge 3 commits intomainfrom
Conversation
Co-Authored-By: kenny <kenny@buildwithfern.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
🌱 Seed Test SelectorSelect languages to run seed tests for:
How to use: Click the ⋯ menu above → "Edit" → check the boxes you want → click "Update comment". Tests will run automatically and snapshots will be committed to this PR. |
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
SDK Generation Benchmark ResultsComparing PR branch against latest nightly baseline on Full benchmark table (click to expand)
main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via |
Docs Generation Benchmark ResultsComparing PR branch against latest nightly baseline on
Docs generation runs |
| // The source of the $ref is the parent of the "$ref" key | ||
| const sourceParent = sourcePath.endsWith(".$ref") ? sourcePath.slice(0, -5) : sourcePath; | ||
| const sourcePointer = dotPathToPointer(sourceParent); | ||
|
|
||
| if (!graph.has(sourcePointer)) { | ||
| graph.set(sourcePointer, new Set()); | ||
| } | ||
| graph.get(sourcePointer)?.add(dotPathToPointer(targetDotPath)); |
There was a problem hiding this comment.
🟡 Cycle detection graph uses mismatched node granularity, failing to detect most indirect circular refs
The findCircularRefs function builds a graph where source nodes are at the $ref location level (e.g., /components/schemas/Foo/properties/bar) but target nodes are at the referenced schema level (e.g., /components/schemas/Bar). These different granularities mean edges never form cycles for the most common circular reference pattern in OpenAPI (indirect references through properties).
Concrete example showing the failure
Given a typical circular ref:
Foo:
properties:
bar:
$ref: '#/components/schemas/Bar'
Bar:
properties:
foo:
$ref: '#/components/schemas/Foo'The graph gets edges:
/components/schemas/Foo/properties/bar→/components/schemas/Bar/components/schemas/Bar/properties/foo→/components/schemas/Foo
Node /components/schemas/Bar has no outgoing edge (only /components/schemas/Bar/properties/foo does), so the DFS never finds a cycle. The function falls through to the fallback message "(unable to determine exact location...)" instead of providing the precise cycle information.
Only direct self-references (e.g., Foo: $ref: '#/.../Foo') or schema-level $ref cycles (where the $ref is the direct child of the schema) are detected.
Prompt for agents
The graph-building logic in findCircularRefs uses the exact $ref location as the source node (e.g. /components/schemas/Foo/properties/bar) but the $ref target as the destination (e.g. /components/schemas/Bar). Since these are at different hierarchy levels, the DFS never finds cycles through properties.
To fix this, the source node should be normalized to the same level as the target. For OpenAPI circular refs, this typically means extracting the schema-level ancestor path from the $ref location. For example, if a $ref lives at components.schemas.Foo.properties.bar.$ref, the source node should be /components/schemas/Foo (not /components/schemas/Foo/properties/bar).
One approach: for each $ref, find the prefix that corresponds to a target path seen elsewhere (like /components/schemas/X) and use that as the source node. Another approach: build the graph specifically at the components/schemas level by matching source paths against the target format (e.g., anything under /components/schemas/X maps to source node /components/schemas/X).
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch — fixed in the next push. The graph now normalizes source nodes to their schema-level ancestor (e.g., components.schemas.Foo.properties.bar → components.schemas.Foo) using a findSchemaAncestor() helper, so indirect cycles through properties are correctly detected. Also added support for definitions.X (Swagger 2.0) paths.
Co-Authored-By: kenny <kenny@buildwithfern.com>
Co-Authored-By: kenny <kenny@buildwithfern.com>
Description
Closes #14422
Gracefully handle the
"Self-referencing circular pointer"error thrown by@redocly/openapi-corewhen bundling OpenAPI specs that contain circular$refreferences. Instead of failing the entire publish/generation pipeline, the CLI now logs a clear warning with the offending file path and circular$refbreadcrumbs, then continues processing with the unbundled document.Changes Made
parseOpenAPI.ts: Wrapped thebundle()call in a try-catch. On"Self-referencing circular pointer"errors, falls back to the unbundled document and continues processing.findCircularRefs.ts): New DFS-based utility that walks the OpenAPI document, builds a schema-level adjacency graph from$reftargets, and detects cycles. Normalizes source nodes to their containing schema ancestor so both direct and indirect circular references are detected.$refchain and test case verifying the full pipeline (workspace loading → IR → FDR) completes successfully despite circular refs.Testing
openapi-from-flag.test.tsLink to Devin session: https://app.devin.ai/sessions/3d60d563398f4f06b4d7328bbf50d3d8