From c79a83e74a0a41367c37f050d78d3e0ace838bc0 Mon Sep 17 00:00:00 2001 From: Abdullah Alaqeel Date: Fri, 26 Jun 2026 19:45:39 +0300 Subject: [PATCH] fix: preserve JSON Schema 2020-12 keyword siblings on $ref schemas for OAS 3.1+ (#2896) * fix: preserve JSON Schema 2020-12 keyword siblings on $ref schemas for OAS 3.1+ OpenApiV31Deserializer.LoadSchema short-circuits on $ref before ParseMap, so sibling keywords ($defs, $dynamicAnchor, $dynamicRef, $id, $anchor, $vocabulary, $comment) were never parsed into the object model. This made Pattern B (generic template + binding) unimplementable for any tool built on Microsoft.OpenApi. The fix mirrors the #2369 annotation-sibling pattern across four coordinated changes: - Parser extraction in SetAdditional31MetadataFromMapNode (scalars + $vocabulary) and LoadSchema ($defs, which needs LoadSchema for nested schema materialization) - Storage: 7 new properties on JsonSchemaReference - Accessor overrides on OpenApiSchemaReference (Reference.X ?? Target?.X) - Serialization in SerializeAdditionalV3XProperties Version-safe by call-site separation: SetAdditional31MetadataFromMapNode is only reachable from V31/V32 LoadSchema, never V3. Ref: microsoft/OpenAPI.NET#2895 * fix: align scalar extraction pattern with existing Title convention Switch from GetPropertyValueFromNode(...) ?? X to the if (!string.IsNullOrEmpty(...)) pattern used by the existing Title/annotation extraction, for reviewer consistency. Also add test for the allOf-based binding variant where $defs sits inside allOf[0] and the nested schema has $ref + $dynamicAnchor (the pattern from the blocker analysis). Ref: microsoft/OpenAPI.NET#2895 * fix: add $schema sibling preservation and fix $defs location tracking Add $schema dialect URI as a sibling override on JsonSchemaReference, matching the pattern used for the other JSON Schema 2020-12 keywords. Also fix the $defs parsing loop in V31/V32 LoadSchema to push/pop the parsing context location stack (context.StartObject/EndObject) around each LoadSchema call, mirroring JsonNodeHelper.CreateMap. Without this, nested schemas inside a reference's $defs get incorrect nodeLocation values, breaking relative $ref resolution and source-pointer diagnostics. Adds a scalar round-trip test covering $id, $schema, $comment, $anchor, $dynamicRef serialization. Ref: microsoft/OpenAPI.NET#2895 * test: add V32 sibling preservation tests mirroring V31 Mirrors the 6 V31 sibling preservation tests in V32Tests, using SerializeAsV32 for the round-trip tests. Parse tests are identical since both versions share the same LoadSchema + SetAdditional31MetadataFromMapNode path. Ref: microsoft/OpenAPI.NET#2895 * fix: don't suppress target values when sibling collections are empty An empty $defs: {} or $vocabulary: {} sibling would assign an empty collection to the reference, blocking fallthrough to Target via the ?? coalescing getter. Only assign when the collection has entries. Ref: microsoft/OpenAPI.NET#2895 * test: add empty-collection fallthrough, 3.0 version safety, and $vocabulary round-trip tests - Empty $defs: {} / $vocabulary: {} must fall through to Target (guards the .Count > 0 fix in commit 4666f2c5) - 3.0 document with $ref + siblings must drop siblings per spec (guards the version-safety guarantee) - $vocabulary round-trip (parse -> serialize -> parse) Ref: microsoft/OpenAPI.NET#2895 * test: add CreateShallowCopy test to cover JsonSchemaReference copy constructor Achieves 100% diff coverage on all changed files. Ref: microsoft/OpenAPI.NET#2895 * fix: address PR review feedback - Replace OpenApiSpecVersion branching with Action callback for future-proof serialization - Add $defs context segment + try/catch error handling in V31/V32 $defs parsing (mirrors ParseField error pattern) - Convert all FluentAssertions calls to Assert.* per repo convention Ref: microsoft/OpenAPI.NET#2896 * chore: refactor to avoid duplication iterating on schemas Signed-off-by: Vincent Biret * tests: reduce the usage of bang operators Signed-off-by: Vincent Biret * tests: use assertions instead of null forgiving to make investigations easier Signed-off-by: Vincent Biret --------- Signed-off-by: Vincent Biret Co-authored-by: Vincent Biret --- .../performance.Descriptions-report-github.md | 20 +- .../performance.Descriptions-report.csv | 12 +- .../performance.Descriptions-report.html | 22 +- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 64 +-- .../performance.EmptyModels-report.csv | 56 +-- .../performance.EmptyModels-report.html | 66 +-- .../performance.EmptyModels-report.json | 2 +- .../Models/JsonSchemaReference.cs | 116 ++++- .../References/OpenApiSchemaReference.cs | 16 +- src/Microsoft.OpenApi/PublicAPI.Unshipped.txt | 16 + .../Reader/V31/OpenApiSchemaDeserializer.cs | 15 +- .../V31Tests/OpenApiSchemaTests.cs | 427 ++++++++++++++++++ 13 files changed, 702 insertions(+), 132 deletions(-) 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 80c323561..d35efb942 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,10 +1,10 @@ ``` -BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8457/25H2/2025Update/HudsonValley2) +BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8655/25H2/2025Update/HudsonValley2) Snapdragon X 12-core X1E80100 3.40 GHz (Max: 3.42GHz), 1 CPU, 12 logical and 12 physical cores -.NET SDK 10.0.300 - [Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), Arm64 RyuJIT armv8.0-a - ShortRun : .NET 8.0.27 (8.0.27, 8.0.2726.22922), Arm64 RyuJIT armv8.0-a +.NET SDK 10.0.301 + [Host] : .NET 8.0.28 (8.0.28, 8.0.2826.26413), Arm64 RyuJIT armv8.0-a + ShortRun : .NET 8.0.28 (8.0.28, 8.0.2826.26413), Arm64 RyuJIT armv8.0-a Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 @@ -12,9 +12,9 @@ WarmupCount=3 ``` | Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |------------- |-------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 362.4 μs | 40.82 μs | 2.24 μs | 74.2188 | 15.6250 | - | 307.15 KB | -| PetStoreJson | 151.2 μs | 17.70 μs | 0.97 μs | 41.0156 | 6.8359 | - | 169.29 KB | -| GHESYaml | 772,063.1 μs | 161,793.80 μs | 8,868.46 μs | 45000.0000 | 18000.0000 | 3000.0000 | 253280.85 KB | -| GHESJson | 304,062.4 μs | 99,068.53 μs | 5,430.28 μs | 18000.0000 | 10000.0000 | 2000.0000 | 110452.47 KB | -| GHESNextYaml | 988,379.0 μs | 43,728.33 μs | 2,396.90 μs | 80000.0000 | 19000.0000 | 3000.0000 | 446980.96 KB | -| GHESNextJson | 558,548.3 μs | 292,614.67 μs | 16,039.20 μs | 52000.0000 | 13000.0000 | 3000.0000 | 308740.81 KB | +| PetStoreYaml | 267.2 μs | 40.45 μs | 2.22 μs | 74.2188 | - | - | 308.02 KB | +| PetStoreJson | 112.4 μs | 21.93 μs | 1.20 μs | 41.5039 | 2.4414 | - | 170.17 KB | +| GHESYaml | 616,153.2 μs | 136,440.25 μs | 7,478.75 μs | 45000.0000 | 18000.0000 | 3000.0000 | 253472.06 KB | +| GHESJson | 252,074.5 μs | 466,491.71 μs | 25,569.98 μs | 18000.0000 | 9000.0000 | 2000.0000 | 110643.88 KB | +| GHESNextYaml | 793,235.7 μs | 226,448.39 μs | 12,412.40 μs | 80000.0000 | 19000.0000 | 3000.0000 | 447183.69 KB | +| GHESNextJson | 460,463.3 μs | 239,580.50 μs | 13,132.22 μs | 53000.0000 | 13000.0000 | 3000.0000 | 308943.27 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index 16b121fac..e61a2efa7 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,362.4 μs,40.82 μs,2.24 μs,74.2188,15.6250,0.0000,307.15 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,151.2 μs,17.70 μs,0.97 μs,41.0156,6.8359,0.0000,169.29 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,"772,063.1 μs","161,793.80 μs","8,868.46 μs",45000.0000,18000.0000,3000.0000,253280.85 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,"304,062.4 μs","99,068.53 μs","5,430.28 μs",18000.0000,10000.0000,2000.0000,110452.47 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,"988,379.0 μs","43,728.33 μs","2,396.90 μs",80000.0000,19000.0000,3000.0000,446980.96 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,"558,548.3 μs","292,614.67 μs","16,039.20 μs",52000.0000,13000.0000,3000.0000,308740.81 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,267.2 μs,40.45 μs,2.22 μs,74.2188,0.0000,0.0000,308.02 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,112.4 μs,21.93 μs,1.20 μs,41.5039,2.4414,0.0000,170.17 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,"616,153.2 μs","136,440.25 μs","7,478.75 μs",45000.0000,18000.0000,3000.0000,253472.06 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,"252,074.5 μs","466,491.71 μs","25,569.98 μs",18000.0000,9000.0000,2000.0000,110643.88 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,"793,235.7 μs","226,448.39 μs","12,412.40 μs",80000.0000,19000.0000,3000.0000,447183.69 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,"460,463.3 μs","239,580.50 μs","13,132.22 μs",53000.0000,13000.0000,3000.0000,308943.27 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 0d32e5521..aab47ee52 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-20260609-152625 +performance.Descriptions-20260626-130032