From a139f83981b251315b55ad9a4d64756a5dead335 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Tue, 14 Jul 2026 14:39:00 +0300 Subject: [PATCH] feat(schema): resolve bare $dynamicRef via $dynamicAnchor index (#2913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(schema): resolve $dynamicRef against $dynamicAnchor via OpenApiSchemaReference Implements document-scoped $dynamicRef resolution per JSON Schema 2020-12 §8.2.3.2. Bare $dynamicRef schemas (no $ref) now deserialize as OpenApiSchemaReference whose Target resolves via per-document $dynamicAnchor and $anchor registries in OpenApiWorkspace. Resolution order in Target: 1. $dynamicAnchor index (single candidate → resolved automatically) 2. $anchor fallback when zero $dynamicAnchor candidates exist (per §8.2.3.2) 3. null when ambiguous (multiple candidates need dynamic-scope tracking) Anchor registries are populated by recursively walking the entire document tree: component schemas, reusable component definitions (parameters, responses, request bodies, headers, callbacks, path items, media types), inline schemas (paths, operations, webhooks), and all nested subschema locations ($defs, properties, items, allOf, if/then/else, etc.). Anchor registration is merged into RegisterSchemaIdentifiers (#2918) so the schema tree is walked once instead of twice. IsDynamicRefOnly is computed from whether $dynamicRef is set and ReferenceV3 does not point to a component path — no field to forget when building from the object model. ResolveDynamicAnchorInContext special-cases OpenApiSchemaReference to read from Reference.DynamicAnchor/Definitions directly, never falling through to Target. Reference holder guards prevent the structural walk from crossing document boundaries through OpenApiParameterReference, OpenApiResponseReference, etc. Test snapshots use a custom JsonConverter that routes schema serialization through the native OpenAPI writer, avoiding reflection cycles from recursive $dynamicRef. Public APIs for consumers tracking dynamic scope: - GetDynamicAnchorCandidates(doc, anchorName): returns all candidate schemas - ResolveDynamicAnchorInContext(contextSchema, anchorName): resolves against a specific entry-point schema's $defs without crossing reference boundaries Known limitation: relative URI resolution in $dynamicRef is not yet implemented (tracked as #2928). Cross-document resolution works for absolute URIs only. * fix(workspace): only consider authored $dynamicAnchor in ResolveDynamicAnchorInContext $defs entries that are themselves $refs no longer fall through to the referenced target's $dynamicAnchor. OpenApiSchemaReference.DynamicAnchor delegates to Target when the authored sibling is empty, which violated the method's context-bound intent. Adds AuthoredDynamicAnchor helper that reads Reference.DynamicAnchor for OpenApiSchemaReference and the plain property otherwise. Both $defs lookups (non-reference context branch and reference-holder branch) now use it. Tests cover both branches in V31 and V32: a Container schema with an aliased $ref def whose Target declares $dynamicAnchor: node, plus a reference holder whose Reference.Definitions carries the same aliased entry. --- .../performance.Descriptions-report-github.md | 12 +- .../performance.Descriptions-report.csv | 12 +- .../performance.Descriptions-report.html | 14 +- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 56 +- .../performance.EmptyModels-report.csv | 56 +- .../performance.EmptyModels-report.html | 58 +- .../performance.EmptyModels-report.json | 2 +- .../Models/JsonSchemaReference.cs | 31 + .../References/OpenApiSchemaReference.cs | 20 + src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 4 + .../Reader/JsonNodeHelper.cs | 46 + .../Reader/V31/OpenApiSchemaDeserializer.cs | 21 + .../Services/OpenApiWorkspace.cs | 456 ++++- .../OpenApiSchemaInterfaceJsonConverter.cs | 28 + .../OpenApiDocumentSerializationTests .cs | 3 +- .../V31Tests/OpenApiDynamicRefTests.cs | 1683 +++++++++++++++++ 17 files changed, 2371 insertions(+), 133 deletions(-) create mode 100644 test/Microsoft.OpenApi.Readers.Tests/OpenApiSchemaInterfaceJsonConverter.cs create mode 100644 test/Microsoft.OpenApi.Readers.Tests/V31Tests/OpenApiDynamicRefTests.cs diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md index e3019f4d3..318ce3a6e 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -12,9 +12,9 @@ WarmupCount=3 ``` | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 232.0 μs | 56.09 μs | 3.07 μs | 76.1719 | 9.7656 | - | 314.13 KB | -| PetStoreJson | 117.3 μs | 10.56 μs | 0.58 μs | 42.9688 | 7.3242 | - | 176.27 KB | -| GHESYaml | 621,674.1 μs | 152,892.95 μs | 8,380.58 μs | 45000.0000 | 19000.0000 | 3000.0000 | 256630.98 KB | -| GHESJson | 264,154.1 μs | 490,035.63 μs | 26,860.51 μs | 18000.0000 | 10000.0000 | 2000.0000 | 113802.94 KB | -| GHESNextYaml | 777,958.3 μs | 269,413.33 μs | 14,767.45 μs | 80000.0000 | 20000.0000 | 3000.0000 | 452706.23 KB | -| GHESNextJson | 432,900.8 μs | 561,774.69 μs | 30,792.77 μs | 54000.0000 | 14000.0000 | 3000.0000 | 314472.13 KB | +| PetStoreYaml | 286.0 μs | 73.19 μs | 4.01 μs | 78.1250 | 3.9063 | - | 321.56 KB | +| PetStoreJson | 138.4 μs | 281.66 μs | 15.44 μs | 44.9219 | 0.9766 | - | 183.7 KB | +| GHESYaml | 634,204.7 μs | 753,841.96 μs | 41,320.62 μs | 46000.0000 | 19000.0000 | 3000.0000 | 258029.89 KB | +| GHESJson | 276,364.3 μs | 194,356.19 μs | 10,653.32 μs | 19000.0000 | 10000.0000 | 2000.0000 | 115201.33 KB | +| GHESNextYaml | 826,374.3 μs | 43,744.86 μs | 2,397.80 μs | 81000.0000 | 20000.0000 | 3000.0000 | 454862.14 KB | +| GHESNextJson | 492,571.5 μs | 71,304.93 μs | 3,908.46 μs | 54000.0000 | 14000.0000 | 3000.0000 | 316622.34 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index c4ac9f16f..a69507ca9 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv @@ -1,7 +1,7 @@ Method,Job,AnalyzeLaunchVariance,EvaluateOverhead,MaxAbsoluteError,MaxRelativeError,MinInvokeCount,MinIterationTime,OutlierMode,Affinity,EnvironmentVariables,Jit,LargeAddressAware,Platform,PowerPlanMode,Runtime,AllowVeryLargeObjects,Concurrent,CpuGroups,Force,HeapAffinitizeMask,HeapCount,NoAffinitize,RetainVm,Server,Arguments,BuildConfiguration,Clock,EngineFactory,NuGetReferences,Toolchain,IsMutator,InvocationCount,IterationCount,IterationTime,LaunchCount,MaxIterationCount,MaxWarmupIterationCount,MemoryRandomization,MinIterationCount,MinWarmupIterationCount,RunStrategy,UnrollFactor,WarmupCount,Mean,Error,StdDev,Gen0,Gen1,Gen2,Allocated -PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,232.0 μs,56.09 μs,3.07 μs,76.1719,9.7656,0.0000,314.13 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,117.3 μs,10.56 μs,0.58 μs,42.9688,7.3242,0.0000,176.27 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"621,674.1 μs","152,892.95 μs","8,380.58 μs",45000.0000,19000.0000,3000.0000,256630.98 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"264,154.1 μs","490,035.63 μs","26,860.51 μs",18000.0000,10000.0000,2000.0000,113802.94 KB -GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"777,958.3 μs","269,413.33 μs","14,767.45 μs",80000.0000,20000.0000,3000.0000,452706.23 KB -GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"432,900.8 μs","561,774.69 μs","30,792.77 μs",54000.0000,14000.0000,3000.0000,314472.13 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,286.0 μs,73.19 μs,4.01 μs,78.1250,3.9063,0.0000,321.56 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,138.4 μs,281.66 μs,15.44 μs,44.9219,0.9766,0.0000,183.7 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"634,204.7 μs","753,841.96 μs","41,320.62 μs",46000.0000,19000.0000,3000.0000,258029.89 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"276,364.3 μs","194,356.19 μs","10,653.32 μs",19000.0000,10000.0000,2000.0000,115201.33 KB +GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"826,374.3 μs","43,744.86 μs","2,397.80 μs",81000.0000,20000.0000,3000.0000,454862.14 KB +GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,111111111111,Empty,RyuJit,Default,Arm64,8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c,.NET 8.0,False,True,False,True,Default,Default,False,False,False,Default,Default,Default,Default,Default,Default,Default,Default,3,Default,1,Default,Default,Default,Default,Default,Default,16,3,"492,571.5 μs","71,304.93 μs","3,908.46 μs",54000.0000,14000.0000,3000.0000,316622.34 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 2fca70ae8..7b541f2d0 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html @@ -2,7 +2,7 @@ -performance.Descriptions-20260708-084315 +performance.Descriptions-20260714-075155