Skip to content

Commit 4e19044

Browse files
baywetCopilot
andcommitted
fix(library): handle circular schema references
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 01be537 commit 4e19044

3 files changed

Lines changed: 100 additions & 13 deletions

File tree

src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Generic;
23

34
namespace Microsoft.OpenApi;
45
/// <summary>
@@ -23,13 +24,24 @@ public T? RecursiveTarget
2324
{
2425
get
2526
{
26-
return Target switch {
27-
BaseOpenApiReferenceHolder<T, U, V> recursiveTarget => recursiveTarget.RecursiveTarget,
28-
T concrete => concrete,
29-
_ => null
30-
};
27+
return ResolveRecursiveTarget(new HashSet<BaseOpenApiReferenceHolder<T, U, V>>());
3128
}
3229
}
30+
31+
private T? ResolveRecursiveTarget(ISet<BaseOpenApiReferenceHolder<T, U, V>> visitedReferences)
32+
{
33+
if (!visitedReferences.Add(this))
34+
{
35+
throw new InvalidOperationException($"Circular reference detected while resolving reference: {Reference.ReferenceV3}");
36+
}
37+
38+
return Target switch
39+
{
40+
BaseOpenApiReferenceHolder<T, U, V> recursiveTarget => recursiveTarget.ResolveRecursiveTarget(visitedReferences),
41+
T concrete => concrete,
42+
_ => null
43+
};
44+
}
3345
/// <summary>
3446
/// Copy the reference as a target element with overrides.
3547
/// </summary>

src/Microsoft.OpenApi/Services/OpenApiWalker.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -881,7 +881,7 @@ internal void Walk(OpenApiEncoding encoding)
881881
/// </summary>
882882
internal void Walk(IOpenApiSchema? schema, bool isComponent = false)
883883
{
884-
if (schema == null || schema is IOpenApiReferenceHolder holder && ProcessAsReference(holder, isComponent))
884+
if (schema == null || schema is IOpenApiReferenceHolder holder && ProcessAsReference(holder))
885885
{
886886
return;
887887
}
@@ -1335,14 +1335,10 @@ private void WalkDictionary<T>(
13351335
/// <summary>
13361336
/// Identify if an element is just a reference to a component, or an actual component
13371337
/// </summary>
1338-
private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, bool isComponent = false)
1338+
private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder)
13391339
{
1340-
var isReference = !isComponent || referenceableHolder.UnresolvedReference;
1341-
if (isReference)
1342-
{
1343-
Walk(referenceableHolder);
1344-
}
1345-
return isReference;
1340+
Walk(referenceableHolder);
1341+
return true;
13461342
}
13471343

13481344
private void WalkTags<T>(ISet<T> tags, Action<OpenApiWalker, T> walk)

test/Microsoft.OpenApi.Readers.Tests/V3Tests/OpenApiDocumentTests.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,85 @@ public async Task ParseMinimalDocumentShouldSucceed()
177177
}, result.Diagnostic);
178178
}
179179

180+
[Fact]
181+
public async Task LoadDocumentWithCircularSchemaPropertyReferencesShouldSucceed()
182+
{
183+
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.yaml");
184+
await File.WriteAllTextAsync(filePath, """
185+
openapi: 3.0.0
186+
info:
187+
title: Test
188+
version: 0.0.1
189+
paths: {}
190+
components:
191+
schemas:
192+
A:
193+
properties:
194+
b:
195+
$ref: '#/components/schemas/B'
196+
B:
197+
properties:
198+
a:
199+
$ref: '#/components/schemas/A'
200+
""");
201+
202+
try
203+
{
204+
var result = await OpenApiDocument.LoadAsync(filePath, SettingsFixture.ReaderSettings);
205+
206+
Assert.NotNull(result.Document);
207+
Assert.Empty(result.Diagnostic.Errors);
208+
209+
var schemaA = Assert.IsType<OpenApiSchema>(result.Document.Components.Schemas["A"]);
210+
var schemaBReference = Assert.IsType<OpenApiSchemaReference>(schemaA.Properties["b"]);
211+
Assert.Same(result.Document.Components.Schemas["B"], schemaBReference.Target);
212+
}
213+
finally
214+
{
215+
File.Delete(filePath);
216+
}
217+
}
218+
219+
[Fact]
220+
public async Task LoadDocumentWithCircularRootSchemaReferencesShouldReturnDiagnosticWarning()
221+
{
222+
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.json");
223+
await File.WriteAllTextAsync(filePath, """
224+
{
225+
"openapi": "3.0.0",
226+
"info": {
227+
"title": "Test",
228+
"version": "0.0.1"
229+
},
230+
"paths": {},
231+
"components": {
232+
"schemas": {
233+
"A": {
234+
"$ref": "#/components/schemas/B"
235+
},
236+
"B": {
237+
"$ref": "#/components/schemas/A"
238+
}
239+
}
240+
}
241+
}
242+
""");
243+
244+
try
245+
{
246+
var result = await OpenApiDocument.LoadAsync(filePath);
247+
248+
Assert.NotNull(result.Document);
249+
Assert.Empty(result.Diagnostic.Errors);
250+
Assert.Contains(result.Diagnostic.Warnings,
251+
warning => warning.Message.StartsWith("Circular reference detected while resolving reference:", StringComparison.Ordinal));
252+
}
253+
finally
254+
{
255+
File.Delete(filePath);
256+
}
257+
}
258+
180259
[Fact]
181260
public async Task ParseStandardPetStoreDocumentShouldSucceed()
182261
{

0 commit comments

Comments
 (0)