Skip to content

Fix/deterministic expansion - #307

Merged
fredbi merged 4 commits into
go-openapi:masterfrom
fredbi:fix/deterministic-expansion
Aug 24, 2026
Merged

Fix/deterministic expansion#307
fredbi merged 4 commits into
go-openapi:masterfrom
fredbi:fix/deterministic-expansion

Conversation

@fredbi

@fredbi fredbi commented Aug 24, 2026

Copy link
Copy Markdown
Member

Change type

Please select: 🆕 New feature or enhancement|🔧 Bug fix'|📃 Documentation update

Short description

Fixes

Full description

Checklist

  • I have signed all my commits with my name and email (see DCO. This does not require a PGP-signed commit
  • I have rebased and squashed my work, so only one commit remains
  • I have added tests to cover my changes.
  • I have properly enriched go doc comments in code.
  • I have properly documented any breaking change.

fredbi and others added 4 commits August 24, 2026 19:39
TestExpandCircular_RemoteCircularID bound localhost:1234 from a goroutine
that panicked when the bind failed, and never closed the server, so the
package could not run under -count>1 - which is what measuring expansion
determinism needs.

testdata/more_circulars/remote/tree and with-id.json both spell the schema
id as http://localhost:1234, so a random port needs the id to follow it.
rewritingFixtureServer serves the embedded FS with that origin replaced by
the address the listener holds; rewriteFixture writes the rewritten spec
into t.TempDir(). The fixtures on disk are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
The file opened with "// -build windows", which is not a build constraint
in any syntax. The _windows suffix already restricts the file, so nothing
changes; the line now reads //go:build windows, like the !windows one in
normalizer_nonwindows.go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
expandSchema walks the fields of Schema and stops there. A keyword the
Swagger 2.0 model predates - propertyNames, contains, if/then/else, $defs -
lands in Schema.ExtraProps as raw JSON, and inlining that subtree from
another document copied its $ref into the root verbatim: "#/definitions/leaf"
then named a definition of the root document rather than the one it came
from, and resolving it raised "has no key".

rebaseExtraRefs walks the raw JSON and rewrites every string under a "$ref"
key with normalizeURI then denormalizeRef, the pair the SkipSchemas branch
already uses for a mapped $ref, so "#/definitions/leaf" becomes
"other.json#/definitions/leaf". The pointer is correct; it is not expanded,
and the output is no more self-contained than before. Empty and unparseable
strings are left alone: an unmapped keyword may hold any JSON, and a string
under a "$ref" key is not necessarily a reference.

Two alternatives were rejected: refusing to expand a $ref whose target the
model cannot represent breaks documents that work today, and expanding the
ExtraProps subtree properly needs a raw-JSON walker with its own cycle
detection over untyped values.

FuzzExpandSpec gains the property that found this - every local $ref
surviving expansion must still name something - guarded by the same check on
the input, so a document that already dangles proves nothing. Its stub loader
used to answer every path, including the root document's own URL, so a $ref
spelled "." inlined a foreign document; it now returns the document's own
bytes for that path.

No cycle is involved: the cut point of shared-node-cycles.json is unchanged
over 40 expansions, with the same three outcomes as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
Expansion inlines the first branch that reaches a cycle and leaves a $ref
on the others, so the order siblings are visited in decides which node
ends up holding the $ref. Nine walks in expander.go ranged a map straight
and handed that decision to Go's map iteration, so the same document
expanded differently from one run to the next: fixture-957.json produced
40 different documents in 40 runs, bitbucket.json 14, and
shared-node-cycles.json 4.

They now walk through sortedKeys, which yields keys in sorted order. The
result is one of the documents the expander already produced - sorting
pins an outcome rather than inventing one - and it is now the only one.

sortedKeys yields straight from the map when it holds fewer than two
keys, since there is then only one order. Most nodes of a large document
are schemata with a single property or definition, and sorting them cost
a slice each: without that fast path expansion was 18% slower overall,
with it the geomean is 9.5% and bitbucket.json is 4% faster than before,
its deterministic order being a better one for the cycle memo.

The residual cost comes from the order, not from the sorting: where the
sorted walk lands on a costlier cut than the average random one it does
more work, +39% on shared-node-cycles.json, and where it lands on a
cheaper one it does less. In exchange the cost stops varying - expanding
fixture-957.json was swinging 44% in time and 46% in allocations between
runs of identical input.

BenchmarkExpandSpec and BenchmarkExpandSpecSkipSchemas measure both paths;
the second is the one analysis takes for minimal and full flattening,
where schemata are not inlined and cycles are never walked.

analysis and validate are green against this, their real-spec corpus
included: their assertions read the form of a surviving $ref and never
which definition holds it.

Fixes go-openapi#93

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.08%. Comparing base (ae6e282) to head (663f784).
⚠️ Report is 2 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
expander.go 95.65% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #307      +/-   ##
==========================================
+ Coverage   93.05%   93.08%   +0.02%     
==========================================
  Files          30       30              
  Lines        2463     2500      +37     
==========================================
+ Hits         2292     2327      +35     
- Misses        171      173       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@fredbi
fredbi merged commit 310c04e into go-openapi:master Aug 24, 2026
24 checks passed
@fredbi
fredbi deleted the fix/deterministic-expansion branch August 24, 2026 17:47
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