Skip to content

Commit ae7c5d9

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

3 files changed

Lines changed: 103 additions & 19 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: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -904,11 +904,17 @@ internal void Walk(OpenApiEncoding encoding)
904904
/// </summary>
905905
internal void Walk(IOpenApiSchema? schema, bool isComponent = false)
906906
{
907-
if (schema == null || schema is IOpenApiReferenceHolder holder && ProcessAsReference(holder, isComponent))
907+
if (schema == null)
908908
{
909909
return;
910910
}
911911

912+
if (schema is IOpenApiReferenceHolder holder)
913+
{
914+
Walk(holder);
915+
return;
916+
}
917+
912918
if (_schemaLoop.Contains(schema))
913919
{
914920
return; // Loop detected, this schema has already been walked.
@@ -1353,19 +1359,6 @@ private void WalkDictionary<T>(
13531359
}
13541360
}
13551361

1356-
/// <summary>
1357-
/// Identify if an element is just a reference to a component, or an actual component
1358-
/// </summary>
1359-
private bool ProcessAsReference(IOpenApiReferenceHolder referenceableHolder, bool isComponent = false)
1360-
{
1361-
var isReference = !isComponent || referenceableHolder.UnresolvedReference;
1362-
if (isReference)
1363-
{
1364-
Walk(referenceableHolder);
1365-
}
1366-
return isReference;
1367-
}
1368-
13691362
private void WalkTags<T>(ISet<T> tags, Action<OpenApiWalker, T> walk)
13701363
where T : IOpenApiTag
13711364
{

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)