From 91a989fdf412a1267debe925d2d586292fdd297d 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 3ea599592..ed2936bd4 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 01172cdd1..9b9235030 100644 --- a/src/Microsoft.OpenApi/Services/OpenApiWalker.cs +++ b/src/Microsoft.OpenApi/Services/OpenApiWalker.cs @@ -904,11 +904,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. @@ -1353,19 +1359,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 c3d1f87d1..595daabae 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 18637f9108f57444e39b90a4f50cf27329a88c22 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 May 2026 15:03:45 +0000 Subject: [PATCH 2/2] chore(benchmark): refresh benchmark reports Agent-Logs-Url: https://github.com/microsoft/OpenAPI.NET/sessions/edaed9f0-284e-4166-b3d8-5a7bd5022b9c Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .../performance.Descriptions-report-github.md | 26 +++---- .../performance.Descriptions-report.csv | 14 ++-- .../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 | 68 +++++++++--------- .../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 1af965d08..d190a20d0 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.8, Windows 11 (10.0.26200.7840/25H2/2025Update/HudsonValley2) -AMD Ryzen 7 7800X3D 4.20GHz, 1 CPU, 16 logical and 8 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) +Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 4 logical and 2 physical cores +.NET SDK 10.0.300 + [Host] : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v4 + ShortRun : .NET 8.0.27 (8.0.27, 8.0.2726.22922), X64 RyuJIT x86-64-v4 Job=ShortRun IterationCount=3 LaunchCount=1 WarmupCount=3 ``` -| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | -|------------- |-------------:|--------------:|-------------:|-----------:|----------:|----------:|-------------:| -| PetStoreYaml | 261.1 μs | 105.89 μs | 5.80 μs | 5.8594 | - | - | 361.38 KB | -| PetStoreJson | 101.9 μs | 48.20 μs | 2.64 μs | 4.3945 | 0.9766 | - | 223.52 KB | -| GHESYaml | 602,932.7 μs | 170,410.86 μs | 9,340.79 μs | 9000.0000 | 8000.0000 | 2000.0000 | 345336.55 KB | -| GHESJson | 254,976.7 μs | 111,875.43 μs | 6,132.27 μs | 4000.0000 | 3000.0000 | 1000.0000 | 206858.06 KB | -| GHESNextYaml | 729,602.0 μs | 357,122.29 μs | 19,575.08 μs | 13000.0000 | 9000.0000 | 2000.0000 | 541566.37 KB | -| GHESNextJson | 378,208.4 μs | 109,458.45 μs | 5,999.79 μs | 8000.0000 | 5000.0000 | 1000.0000 | 406762.41 KB | +| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|------------- |---------------:|--------------:|------------:|-----------:|-----------:|----------:|-------------:| +| PetStoreYaml | 480.2 μs | 50.16 μs | 2.75 μs | 11.7188 | - | - | 363.98 KB | +| PetStoreJson | 215.4 μs | 27.68 μs | 1.52 μs | 8.7891 | 1.9531 | - | 225.84 KB | +| GHESYaml | 1,034,639.0 μs | 99,755.13 μs | 5,467.92 μs | 17000.0000 | 14000.0000 | 3000.0000 | 345962.32 KB | +| GHESJson | 414,562.3 μs | 57,822.66 μs | 3,169.46 μs | 8000.0000 | 6000.0000 | 1000.0000 | 207483.96 KB | +| GHESNextYaml | 1,233,870.1 μs | 138,213.19 μs | 7,575.93 μs | 25000.0000 | 15000.0000 | 3000.0000 | 542594.95 KB | +| GHESNextJson | 649,688.9 μs | 72,965.05 μs | 3,999.46 μs | 16000.0000 | 8000.0000 | 1000.0000 | 407786.32 KB | diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.csv index 963b9ce2e..8ce3ff619 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;1111111111111111;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;261.1 μs;105.89 μs;5.80 μs;5.8594;0.0000;0.0000;361.38 KB -PetStoreJson;ShortRun;False;Default;Default;Default;Default;Default;Default;1111111111111111;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;101.9 μs;48.20 μs;2.64 μs;4.3945;0.9766;0.0000;223.52 KB -GHESYaml;ShortRun;False;Default;Default;Default;Default;Default;Default;1111111111111111;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;"602,932.7 μs";"170,410.86 μs";"9,340.79 μs";9000.0000;8000.0000;2000.0000;345336.55 KB -GHESJson;ShortRun;False;Default;Default;Default;Default;Default;Default;1111111111111111;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;"254,976.7 μs";"111,875.43 μs";"6,132.27 μs";4000.0000;3000.0000;1000.0000;206858.06 KB -GHESNextYaml;ShortRun;False;Default;Default;Default;Default;Default;Default;1111111111111111;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;"729,602.0 μs";"357,122.29 μs";"19,575.08 μs";13000.0000;9000.0000;2000.0000;541566.37 KB -GHESNextJson;ShortRun;False;Default;Default;Default;Default;Default;Default;1111111111111111;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;"378,208.4 μs";"109,458.45 μs";"5,999.79 μs";8000.0000;5000.0000;1000.0000;406762.41 KB +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,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,480.2 μs,50.16 μs,2.75 μs,11.7188,0.0000,0.0000,363.98 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,215.4 μs,27.68 μs,1.52 μs,8.7891,1.9531,0.0000,225.84 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,034,639.0 μs","99,755.13 μs","5,467.92 μs",17000.0000,14000.0000,3000.0000,345962.32 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,"414,562.3 μs","57,822.66 μs","3,169.46 μs",8000.0000,6000.0000,1000.0000,207483.96 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,233,870.1 μs","138,213.19 μs","7,575.93 μs",25000.0000,15000.0000,3000.0000,542594.95 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,"649,688.9 μs","72,965.05 μs","3,999.46 μs",16000.0000,8000.0000,1000.0000,407786.32 KB diff --git a/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html b/performance/benchmark/BenchmarkDotNet.Artifacts/results/performance.Descriptions-report.html index a5a17b789..9cd26a52f 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-20260223-221211 +performance.Descriptions-20260526-145612