Skip to content

fix: namespace external-document pointers so same-named schemas don't conflate (#358) - #364

Open
eseidel wants to merge 2 commits into
mainfrom
es/multidoc-pointer-collision
Open

eseidel wants to merge 2 commits into
mainfrom
es/multidoc-pointer-collision

Conversation

@eseidel

@eseidel eseidel commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Fix multi-document schema-name collision (#358): two documents each
defining #/components/schemas/Foo used to conflate into one type. The
pipeline keys identity on JsonPointer, but an external OpenAPI-shaped
components library was parsed with a document-relative pointer, so
its Foo got the same #/components/schemas/Foo the root's Foo has.
Generation didn't fail — it silently emitted one foo.dart for two
types (whichever the walk reached first won).

JsonPointer gains an optional baseUri — the document it lives in —
that participates in equality but never in urlEncodedFragment.
Root pointers keep baseUri: null; the external components-library
parse threads baseUri: docUri. The two Foos become distinct
pointers → distinct types → distinct files, while the ref-resolvable
fragment (hence the registry key a $ref resolves against) is unchanged.

The root keeps the bare Foo; the external one disambiguates to Foo1
(foo_1.dart) — the same _1 convention an in-document name collision
already uses (Foo-Bar + Foo_BarFooBar + FooBar1), rather than
the name allocator's generic Foo2 fallback.

Details

  • lib/src/types.dartJsonPointer.fromParts({Uri? baseUri}), carried
    through add, added to props, left out of urlEncodedFragment.
  • lib/src/parser.dartParseContext.baseUri threads through
    MapContext/ListContext child contexts into the pointer getter.
  • lib/src/assemble.dart — the OpenAPI-shaped components-library parse
    sets baseUri: docUri. Split-spec targets already avoided this by
    keying on the full URI; this brings the components-library branch in
    line.
  • lib/src/resolver.dart_collectNames folds external-document
    schemas from the assembled registry into collision resolution, so a
    cross-document collision disambiguates through the same _1 path as an
    in-document one (root keeps the bare name; only external schemas are
    folded in, so the registry's copies of root schemas don't double-count).

Verification

  • New unit tests on JsonPointer identity (baseUri disambiguates
    equality but not the emitted fragment) and add preservation.
  • New end-to-end test: a root spec + external components library both
    defining Foo now emit Foo (localField, foo.dart) and Foo1
    (externalField, foo_1.dart) as distinct, analyze-clean types.
    Confirmed the test fails without the fix and passes with it.
  • Byte-identical on every tracked fixture — tool/gen_tests.dart
    regens github/discord/backstage/petstore/spacetraders/train-travel
    with zero diff (single-document specs are all baseUri: null with no
    external schemas to fold in).

Known limitation (follow-up)

A discriminator mapping still rebuilds its match pointer from the ref
string (JsonPointer.parse, base-null), so a discriminator declared in
an external document remains a separate, pre-existing limitation
untouched here.

… conflate (#358)

Two documents each defining `#/components/schemas/Foo` used to conflate
into one type: the pipeline keys identity on `JsonPointer`, but an
external OpenAPI-shaped components library was parsed with a
document-relative pointer, so its `Foo` got the same
`#/components/schemas/Foo` the root's `Foo` has. Generation didn't fail
— it emitted one `foo.dart` for two types (whichever the walk reached
first won).

Give `JsonPointer` an optional `baseUri` — the document it lives in —
that participates in equality but never in `urlEncodedFragment`. Root
pointers keep `baseUri: null`; the external components-library parse
threads `baseUri: docUri`. So the two `Foo`s become distinct pointers →
distinct names (the existing `_1`/`2` disambiguation) → distinct files,
while the ref-resolvable fragment (hence the registry key) is unchanged.
Single-document output is byte-identical — every tracked fixture regens
with zero diff.

Split-spec targets already avoided this (they key on the full URI); this
brings the components-library branch in line. A discriminator mapping
still rebuilds its match pointer from the ref string
(`JsonPointer.parse`, base-null), so an external-document discriminator
remains a separate, pre-existing limitation.
@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.69%. Comparing base (76adedc) to head (926695c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #364      +/-   ##
==========================================
+ Coverage   96.53%   96.69%   +0.15%     
==========================================
  Files          25       25              
  Lines        5888     5893       +5     
==========================================
+ Hits         5684     5698      +14     
+ Misses        204      195       -9     

☔ View full report in Codecov by Harness.
📢 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.

…vention

Follow-up to the pointer-identity fix in the same PR. With distinct
pointers, the second `Foo` was named by the render-side name allocator's
generic fallback (`Foo2`, `foo2.dart`) because the collision resolver
only walked the root document and never saw the external schema.

Fold external-document schemas (`baseUri != null`) from the assembled
registry into `_collectNames`, after the root walk. The two `Foo`s now
disambiguate through the same path an in-document collision uses: the
root keeps the bare `Foo`, the external one becomes `Foo1` (`foo_1.dart`)
— matching `Foo-Bar` + `Foo_Bar` -> `FooBar` + `FooBar1`. Only external
schemas are folded in; the registry's copies of the root's own schemas
are skipped so they don't double-count into a false self-collision.

Byte-identical on every tracked fixture (single-document specs have no
external schemas to fold in).
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.

1 participant