Skip to content

fix: Fix duplicate schemas from root document back-references (#1961) - #2383

Merged
ewaostrowska merged 2 commits into
masterfrom
issue-1961
Aug 25, 2026
Merged

fix: Fix duplicate schemas from root document back-references (#1961)#2383
ewaostrowska merged 2 commits into
masterfrom
issue-1961

Conversation

@ewaostrowska

@ewaostrowska ewaostrowska commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Description

When an OpenAPI file has external $refs that point back to the root document, the parser produces duplicate schemas.

Example:

TestCase.yaml
  └─ $ref: ./TestCase.v1.yaml#/components/schemas/TestCase_v1_TestCase

TestCase.v1.yaml
  └─ $ref: ./TestCase.yaml#/components/schemas/TestCase_Foo   ← back to root

The parser treated ./TestCase.yaml as a new external file, loaded it from disk again, and when it found TestCase_Foo already in components, renamed the imported copy TestCase_Foo_1.

Root cause: path comparison used plain string matching. ./TestCase.yaml and src/test/resources/TestCase.yaml look different as strings even though they point to the same file.


Solution: root document identity

ResolverCache now stores a rootDocumentUri — a normalized URI computed once at construction time. Every external $ref is checked against it before loading.

PathUtils.rootDocumentUri(String) handles three input forms:

Input Example Result
HTTP/HTTPS URL https://example.com/api/../root.yaml#section https://example.com/root.yaml
file: URI file:///home/user/specs/../root.yaml file:///home/user/root.yaml
Filesystem path src/test/resources/root.yaml file:///abs/path/to/root.yaml

Unsupported formats (classpath:, jar:file:) return null, which disables the check and preserves the original behavior.


How the check works

isRootDocument(file) runs three steps on every external file load:

  1. Resolve the external path against rootDocumentUri to get an absolute URI
  2. Strip the #fragment (the file is the same regardless of which part is referenced)
  3. Compare to rootDocumentUri — equal means it's a back-reference to root

When a back-reference is detected, the parser calls loadInternalRef("#/" + definitionPath) — the same lookup used for internal #/components/... references — and returns the already-loaded object from memory. No file re-read, no deserialization, no duplicate.

If the object is not found or has the wrong type, it falls back to deserializing from the cached file content.


Other changes

PathUtils helpers (moved out of ResolverCache for reusability):

  • rootDocumentUri(String) — normalize any supported path to a URI
  • parentDirectoryOfUri(URI) — get the parent folder of a file: URI
  • isHttpUri(URI) — check for http/https scheme
  • withoutFragment(URI) — strip the #fragment from a URI

This also clears a long-standing TODO in PathUtils: "TODO use properly java URL to identify absolute URL."

Parent directory resolution for file: URIs now uses PathUtils.parentDirectoryOfUri(URI), which strips the query string and fragment before returning the parent folder. Behavior for HTTP/HTTPS and other path types is unchanged.
Fixes: #1961

Why the root snapshot is needed

Resolution copies external components into the root OpenAPI model, so the live component map ends up containing both original and imported entries. ResolverCache saves the original root components before resolution starts. A back-reference to the root can only reuse entries from that snapshot.

For example, if the root originally contains only Local, an imported Payload cannot make this reference valid:
./root.yaml#/components/schemas/Payload
The parser reads the root file and reports the missing schema. This keeps valid root back-references deduplicated and prevents resolution order from affecting results.

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor (non-breaking change)
  • 🧪 Tests
  • 📝 Documentation
  • 🧹 Chore (build or tooling)

Checklist

  • I have added/updated tests as needed
  • I have added/updated documentation where applicable
  • The PR title is descriptive
  • The code builds and passes tests locally
  • I have linked related issues (if any)

Screenshots / Additional Context

@ewaostrowska
ewaostrowska merged commit 0255d18 into master Aug 25, 2026
7 checks passed
@ewaostrowska
ewaostrowska deleted the issue-1961 branch August 25, 2026 06:24
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.

Duplicated schemas produced when shared schema references local schema

2 participants