From 6bcac3995314f71028d234270abd6fc4773aabf6 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Tue, 26 May 2026 07:12:29 -0400 Subject: [PATCH 1/2] fix(library): handle circular schema references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../References/BaseOpenApiReferenceHolder.cs | 22 +++-- .../Services/OpenApiWalker.cs | 21 ++--- .../Reader/OpenApiModelFactoryTests.cs | 90 +++++++++++++++++++ 3 files changed, 114 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs index 634c5a723..d28e9dd8f 100644 --- a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs +++ b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Microsoft.OpenApi; /// @@ -23,13 +24,24 @@ public T? RecursiveTarget { get { - return Target switch { - BaseOpenApiReferenceHolder recursiveTarget => recursiveTarget.RecursiveTarget, - T concrete => concrete, - _ => null - }; + return ResolveRecursiveTarget(new HashSet>()); } } + + private T? ResolveRecursiveTarget(ISet> visitedReferences) + { + if (!visitedReferences.Add(this)) + { + throw new InvalidOperationException($"Circular reference detected while resolving reference: {Reference.ReferenceV3}"); + } + + return Target switch + { + BaseOpenApiReferenceHolder recursiveTarget => recursiveTarget.ResolveRecursiveTarget(visitedReferences), + T concrete => concrete, + _ => null + }; + } /// /// Copy the reference as a target element with overrides. /// diff --git a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs index 315146102..8239b2dbc 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -881,11 +881,17 @@ internal void Walk(OpenApiEncoding encoding) /// internal void Walk(IOpenApiSchema? schema, bool isComponent = false) { - if (schema == null || schema is IOpenApiReferenceHolder holder && ProcessAsReference(holder, isComponent)) + if (schema == null) { return; } + if (schema is IOpenApiReferenceHolder holder) + { + Walk(holder); + return; + } + if (_schemaLoop.Contains(schema)) { return; // Loop detected, this schema has already been walked. @@ -1332,19 +1338,6 @@ private void WalkDictionary( } } - /// - /// Identify if an element is just a reference to a component, or an actual component - /// - private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, bool isComponent = false) - { - var isReference = !isComponent || referenceableHolder.UnresolvedReference; - if (isReference) - { - Walk(referenceableHolder); - } - return isReference; - } - private void WalkTags(ISet tags, Action walk) where T : IOpenApiTag { diff --git a/test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs b/test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs index c51d3e9db..9a28fb126 100644 --- a/test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs +++ b/test/Microsoft.OpenApi.Tests/Reader/OpenApiModelFactoryTests.cs @@ -9,6 +9,96 @@ namespace Microsoft.OpenApi.Tests.Reader; public class OpenApiModelFactoryTests { + [Fact] + public async Task LoadDocumentWithCircularSchemaPropertyReferencesShouldSucceed() + { + var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.json"); + await File.WriteAllTextAsync(filePath, """ +{ + "openapi": "3.0.0", + "info": { + "title": "Test", + "version": "0.0.1" + }, + "paths": {}, + "components": { + "schemas": { + "A": { + "properties": { + "b": { + "$ref": "#/components/schemas/B" + } + } + }, + "B": { + "properties": { + "a": { + "$ref": "#/components/schemas/A" + } + } + } + } + } +} +"""); + + try + { + var result = await OpenApiDocument.LoadAsync(filePath); + + Assert.NotNull(result.Document); + Assert.Empty(result.Diagnostic.Errors); + + var schemaA = Assert.IsType(result.Document.Components.Schemas["A"]); + var schemaBReference = Assert.IsType(schemaA.Properties["b"]); + Assert.Same(result.Document.Components.Schemas["B"], schemaBReference.Target); + } + finally + { + File.Delete(filePath); + } + } + + [Fact] + public async Task LoadDocumentWithCircularRootSchemaReferencesShouldReturnDiagnosticWarning() + { + var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.json"); + await File.WriteAllTextAsync(filePath, """ +{ + "openapi": "3.0.0", + "info": { + "title": "Test", + "version": "0.0.1" + }, + "paths": {}, + "components": { + "schemas": { + "A": { + "$ref": "#/components/schemas/B" + }, + "B": { + "$ref": "#/components/schemas/A" + } + } + } +} +"""); + + try + { + var result = await OpenApiDocument.LoadAsync(filePath); + + Assert.NotNull(result.Document); + Assert.Empty(result.Diagnostic.Errors); + Assert.Contains(result.Diagnostic.Warnings, + warning => warning.Message.StartsWith("Circular reference detected while resolving reference:", StringComparison.Ordinal)); + } + finally + { + File.Delete(filePath); + } + } + [Fact] public async Task UsesSettingsBaseUrl() { From 8a30207259ac1f6152bb1cb3fb06529658673e69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 15:25:09 +0000 Subject: [PATCH 2/2] chore(benchmark): refresh benchmark reports Agent-Logs-Url: https://github.com/microsoft/OpenAPI.NET/sessions/e70b16a8-7afe-4f05-abac-6fc6bcd50960 Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../performance.Descriptions-report-github.md | 26 +++---- .../performance.Descriptions-report.csv | 12 ++-- .../performance.Descriptions-report.html | 26 +++---- .../performance.Descriptions-report.json | 2 +- .../performance.EmptyModels-report-github.md | 70 +++++++++---------- .../performance.EmptyModels-report.csv | 58 +++++++-------- .../performance.EmptyModels-report.html | 70 +++++++++---------- .../performance.EmptyModels-report.json | 2 +- 8 files changed, 133 insertions(+), 133 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 a1ea830bf..36532861e 100644 --- a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md +++ b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report-github.md @@ -1,20 +1,20 @@ ``` -BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7781) -11th Gen Intel Core i7-1185G7 3.00GHz, 1 CPU, 8 logical and 4 physical cores -.NET SDK 8.0.418 - [Host] : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4 - ShortRun : .NET 8.0.24 (8.0.24, 8.0.2426.7010), X64 RyuJIT x86-64-v4 +BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) +AMD EPYC 7763 2.79GHz, 1 CPU, 4 logical and 2 physical cores +.NET SDK 10.0.204 + [Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3 + ShortRun : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v3 Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |---------------:|----------------:|-------------:|-----------:|-----------:|----------:|-------------:| -| PetStoreYaml | 881.3 μs | 2,728.99 μs | 149.58 μs | 54.6875 | 7.8125 | - | 360.94 KB | -| PetStoreJson | 190.5 μs | 60.34 μs | 3.31 μs | 36.1328 | 7.8125 | - | 223.09 KB | -| GHESYaml | 1,041,152.3 μs | 128,977.22 μs | 7,069.68 μs | 60000.0000 | 23000.0000 | 4000.0000 | 345284.59 KB | -| GHESJson | 464,809.9 μs | 253,196.41 μs | 13,878.55 μs | 33000.0000 | 12000.0000 | 2000.0000 | 206806.4 KB | -| GHESNextYaml | 1,268,192.5 μs | 1,209,245.90 μs | 66,282.85 μs | 91000.0000 | 22000.0000 | 3000.0000 | 541026.33 KB | -| GHESNextJson | 749,410.0 μs | 1,057,535.55 μs | 57,967.09 μs | 65000.0000 | 20000.0000 | 2000.0000 | 406224.57 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |---------------:|--------------:|-------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 528.6 μs | 178.98 μs | 9.81 μs | 19.5313 | 3.9063 | - | 363.54 KB | +| PetStoreJson | 232.1 μs | 12.90 μs | 0.71 μs | 13.6719 | 1.9531 | - | 225.39 KB | +| GHESYaml | 1,072,146.4 μs | 200,482.34 μs | 10,989.11 μs | 24000.0000 | 19000.0000 | 3000.0000 | 345905.05 KB | +| GHESJson | 483,939.6 μs | 288,033.88 μs | 15,788.11 μs | 13000.0000 | 9000.0000 | 2000.0000 | 207426.91 KB | +| GHESNextYaml | 1,325,231.0 μs | 240,938.48 μs | 13,206.65 μs | 36000.0000 | 20000.0000 | 3000.0000 | 542041.88 KB | +| GHESNextJson | 699,920.8 μs | 85,692.01 μs | 4,697.07 μs | 25000.0000 | 11000.0000 | 2000.0000 | 407243.96 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index 3241d2b2f..7bf9078cd 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,11111111,Empty,RyuJit,Default,X64,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,881.3 μs,"2,728.99 μs",149.58 μs,54.6875,7.8125,0.0000,360.94 KB -PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,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,190.5 μs,60.34 μs,3.31 μs,36.1328,7.8125,0.0000,223.09 KB -GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,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,"1,041,152.3 μs","128,977.22 μs","7,069.68 μs",60000.0000,23000.0000,4000.0000,345284.59 KB -GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,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,"464,809.9 μs","253,196.41 μs","13,878.55 μs",33000.0000,12000.0000,2000.0000,206806.4 KB -GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,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,"1,268,192.5 μs","1,209,245.90 μs","66,282.85 μs",91000.0000,22000.0000,3000.0000,541026.33 KB -GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,11111111,Empty,RyuJit,Default,X64,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,"749,410.0 μs","1,057,535.55 μs","57,967.09 μs",65000.0000,20000.0000,2000.0000,406224.57 KB +PetStoreYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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,528.6 μs,178.98 μs,9.81 μs,19.5313,3.9063,0.0000,363.54 KB +PetStoreJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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.1 μs,12.90 μs,0.71 μs,13.6719,1.9531,0.0000,225.39 KB +GHESYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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,"1,072,146.4 μs","200,482.34 μs","10,989.11 μs",24000.0000,19000.0000,3000.0000,345905.05 KB +GHESJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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,"483,939.6 μs","288,033.88 μs","15,788.11 μs",13000.0000,9000.0000,2000.0000,207426.91 KB +GHESNextYaml,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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,"1,325,231.0 μs","240,938.48 μs","13,206.65 μs",36000.0000,20000.0000,3000.0000,542041.88 KB +GHESNextJson,ShortRun,False,Default,Default,Default,Default,Default,Default,1111,Empty,RyuJit,Default,X64,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,"699,920.8 μs","85,692.01 μs","4,697.07 μs",25000.0000,11000.0000,2000.0000,407243.96 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index 65c1bb4b9..a694be1ea 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-20260224-161326 +performance.Descriptions-20260526-151849