Skip to content

Fix dangling OpenApiSchemaReference on net10 (#30) - #32

Merged
panesofglass merged 11 commits into
masterfrom
30-openapi-dangling-schema-ref
Jul 28, 2026
Merged

Fix dangling OpenApiSchemaReference on net10 (#30)#32
panesofglass merged 11 commits into
masterfrom
30-openapi-dangling-schema-ref

Conversation

@panesofglass

Copy link
Copy Markdown
Collaborator

Summary

  • FSharpSchemaTransformer never registered its component schemas into the live OpenApiDocument on net10, and built every reference with a null host document, so references never resolved once Microsoft.AspNetCore.OpenApi 10.0.3+ started walking and resolving the schema tree strictly after transformers run.
  • Also fixes self-recursive types all resolving their self-reference to the same hardcoded component id ("root").
  • Scoped to the net10 / Microsoft.OpenApi 2.x path; net9 is unaffected and untouched.

Closes #30.

Test plan

Design doc: docs/superpowers/specs/2026-07-25-openapi-dangling-schema-ref-design.md

panesofglass and others added 11 commits July 25, 2026 16:55
Root-caused via local repro against Microsoft.AspNetCore.OpenApi 10.0.10:
OpenApiSchemaTranslator constructs OpenApiSchemaReference with a null host
document and FSharpSchemaTransformer never registers component schemas into
the live OpenApiDocument, so references never resolve once ASP.NET 10.0.3+
started walking and resolving the schema tree strictly after transformers run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…#30)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The floating 10.0.0-* reference resolved to exactly 10.0.0, the one
version that predates the breaking schema-resolution strictness change
in #30. Pinning to a current version so tests actually exercise the
code path that broke.
Real WebApplication + MapOpenApi + HTTP GET /openapi/v1.json, for both
a plain DU and a self-recursive DU. Fails on net10 with the exact #30
exception; passes on net9, confirming the bug and the fix are scoped
to the net10 / Microsoft.OpenApi 2.x path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds translateForDocument, which registers component schemas (including
the root schema itself under a supplied rootTypeId) into a live
OpenApiDocument and binds references to it via document.AddComponent /
OpenApiSchemaReference(id, document), so .Target actually resolves.
Also fixes self-ref ("#") ids: they previously always resolved to the
literal string "root" (rootSchema.Title was never set), which would
have collided across different self-recursive types sharing a document
once references were actually registered. translate(doc) is unchanged
for existing callers.
FSharpSchemaTransformer now calls translateForDocument with the live
context.Document and the root type's id (via SchemaGeneratorConfig's
existing TypeIdResolver) on net10, so component schemas are actually
registered and references resolve instead of dangling. This is what
made Microsoft.AspNetCore.OpenApi 10.0.3+ throw on any DU or
self-recursive type.
Add e2e test proving two different self-recursive types on the same
document register distinct components without collision. Document the
AddComponent id-ownership assumption and the null-Document fallback's
relationship to #30. Correct RELEASE_NOTES to scope the self-ref id
fix to net10.
document.AddComponent is TryAdd (first writer wins, never throws), and
component ids were bare case/definition names — so two different F#
types sharing a case name (e.g. both having a "Leaf" or "Error" case)
would silently collide in the same OpenAPI document, with the second
type's schema never actually registering under its own definition.

Qualify every definition id with its owning root type's name
(rootTypeId + "." + typeId) when bound to a live document; the
document-agnostic translate() entry point is unaffected (qualify is
identity when document = None), so all pre-existing structural tests
keep passing unchanged. The "." separator is required, not cosmetic:
without it, different (rootTypeId, typeId) pairs can concatenate to
the same string (e.g. "Order"+"LineItem" = "OrderLine"+"Item").

Also addresses the remaining deferred Minor findings from final
review: precondition guard on rootTypeId, doc-comment scope fixes,
restored net9 comment, and a WebApplication dispose leak in the e2e
test fixtures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The transitive default (2.0.0, floored by Microsoft.AspNetCore.OpenApi
10.0.10) is affected by GHSA-v5pm-xwqc-g5wc (stack overflow parsing
circular schema refs in untrusted documents; patched at 2.7.5+). We
only generate documents, never parse untrusted ones, so exposure was
low, but pinning above the patched version costs nothing. net9's
transitive Microsoft.OpenApi 1.x line is unaffected per the advisory
and untouched.

Verified: full OpenApi suite green on both net9.0 (22/22) and net10.0
(25/25) at the new pinned version; the AddComponent/OpenApiSchemaReference/
Target-resolution APIs this fix depends on behave identically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Generator.CreateInternal's closure calls into Namotion.Reflection (via
SchemaNameGenerator.Generate -> ty.ToCachedType(), and NJsonSchema's own
base generator) with no synchronization. Namotion.Reflection keeps
global, non-thread-safe type-metadata caches, so concurrent schema
generation for different types -- exactly what Expecto's parallel test
runner does -- intermittently threw "_type is not initialized" from
CachedType.get_Type(), a different specific test failing each run.

Reproduced on unmodified master (a2f4557, before any #30 work): 3/8
runs failed, three different tests, same signature -- confirming this
predates and is unrelated to the #30 branch. Wrapping the generation
closure in a lock removes the concurrent-access trigger without
touching the third-party dependency: 15/15 clean runs after the fix
(vs 3/8 failures before), confirmed on net8.0/net9.0/net10.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@panesofglass
panesofglass merged commit 6e0d8e8 into master Jul 28, 2026
1 check passed
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.

OpenApi: The input schema must be an OpenApiSchema or OpenApiSchemaReference

1 participant