diff --git a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
index abf6a0748..031eadb5d 100644
--- a/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
+++ b/src/Microsoft.OpenApi/Models/OpenApiDocument.cs
@@ -498,10 +498,10 @@ public void SetReferenceHostDocument()
///
/// Load the referenced object from a object
///
- internal T? ResolveReferenceTo(BaseOpenApiReference reference) where T : IOpenApiReferenceable
+ internal T? ResolveReferenceTo(BaseOpenApiReference reference, IOpenApiSchema? parentSchema) where T : IOpenApiReferenceable
{
- if (ResolveReference(reference, reference.IsExternal) is T result)
+ if (ResolveReference(reference, reference.IsExternal, parentSchema) is T result)
{
return result;
}
@@ -566,7 +566,7 @@ private static string ConvertByteArrayToString(byte[] hash)
///
/// Load the referenced object from a object
///
- internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal)
+ internal IOpenApiReferenceable? ResolveReference(BaseOpenApiReference? reference, bool useExternal, IOpenApiSchema? parentSchema)
{
if (reference == null)
{
@@ -621,9 +621,9 @@ private static string ConvertByteArrayToString(byte[] hash)
false => new Uri(uriLocation).AbsoluteUri
};
- if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#'))
+ if (reference.Type is ReferenceType.Schema && absoluteUri.Contains('#') && parentSchema is not null)
{
- return Workspace?.ResolveJsonSchemaReference(absoluteUri);
+ return Workspace?.ResolveJsonSchemaReference(absoluteUri, parentSchema);
}
return Workspace?.ResolveReference(absoluteUri);
diff --git a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
index 3f90276ff..634c5a723 100644
--- a/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
+++ b/src/Microsoft.OpenApi/Models/References/BaseOpenApiReferenceHolder.cs
@@ -15,7 +15,7 @@ public virtual U? Target
get
{
if (Reference.HostDocument is null) return default;
- return Reference.HostDocument.ResolveReferenceTo(Reference);
+ return Reference.HostDocument.ResolveReferenceTo(Reference, this as IOpenApiSchema);
}
}
///
diff --git a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
index 65fccb15a..ad1ea8aa2 100644
--- a/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
+++ b/src/Microsoft.OpenApi/Services/OpenApiWorkspace.cs
@@ -331,8 +331,9 @@ public bool Contains(string location)
/// Recursively resolves a schema from a URI fragment.
///
///
+ /// The parent schema to resolve against.
///
- internal IOpenApiSchema? ResolveJsonSchemaReference(string location)
+ internal IOpenApiSchema? ResolveJsonSchemaReference(string location, IOpenApiSchema parentSchema)
{
/* Enables resolving references for nested subschemas
* Examples:
@@ -362,14 +363,22 @@ public bool Contains(string location)
{
// traverse remaining segments after fetching the base schema
var remainingSegments = pathSegments.Skip(4).ToArray();
- return ResolveSubSchema(targetSchema, remainingSegments);
+ var stack = new Stack();
+ stack.Push(parentSchema);
+ return ResolveSubSchema(targetSchema, remainingSegments, stack);
}
return default;
}
- internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments)
+ internal static IOpenApiSchema? ResolveSubSchema(IOpenApiSchema schema, string[] pathSegments, Stack visitedSchemas)
{
+ // Prevent infinite recursion in case of circular references
+ if (visitedSchemas.Contains(schema))
+ {
+ return null;
+ }
+ visitedSchemas.Push(schema);
// Traverse schema object to resolve subschemas
if (pathSegments.Length == 0)
{
@@ -383,13 +392,13 @@ public bool Contains(string location)
case OpenApiConstants.Properties:
var propName = pathSegments[0];
if (schema.Properties != null && schema.Properties.TryGetValue(propName, out var propSchema))
- return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)]);
+ return ResolveSubSchema(propSchema, [.. pathSegments.Skip(1)], visitedSchemas);
break;
case OpenApiConstants.Items:
- return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments) : null;
+ return schema.Items is OpenApiSchema itemsSchema ? ResolveSubSchema(itemsSchema, pathSegments, visitedSchemas) : null;
case OpenApiConstants.AdditionalProperties:
- return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments) : null;
+ return schema.AdditionalProperties is OpenApiSchema additionalSchema ? ResolveSubSchema(additionalSchema, pathSegments, visitedSchemas) : null;
case OpenApiConstants.AllOf:
case OpenApiConstants.AnyOf:
case OpenApiConstants.OneOf:
@@ -405,7 +414,7 @@ public bool Contains(string location)
// recurse into the indexed subschema if valid
if (list != null && index < list.Count)
- return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)]);
+ return ResolveSubSchema(list[index], [.. pathSegments.Skip(1)], visitedSchemas);
break;
}
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs
index 5845e4bbf..36ecdc849 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V2Tests/OpenApiDocumentTests.cs
@@ -261,7 +261,7 @@ public async Task ShouldAllowComponentsThatJustContainAReference()
var schema1 = actual.Components.Schemas["AllPets"];
var schema1Reference = Assert.IsType(schema1);
Assert.False(schema1Reference.UnresolvedReference);
- var schema2 = actual.ResolveReferenceTo(schema1Reference.Reference);
+ var schema2 = actual.ResolveReferenceTo(schema1Reference.Reference, schema1Reference);
Assert.IsType(schema2);
if (string.IsNullOrEmpty(schema1Reference.Reference.Id) || schema1Reference.UnresolvedReference)
{
diff --git a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs
index 70b0c96a9..c32f63664 100644
--- a/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs
+++ b/test/Microsoft.OpenApi.Readers.Tests/V31Tests/RelativeReferenceTests.cs
@@ -223,7 +223,7 @@ public void ResolveSubSchema_ShouldTraverseKnownKeywords()
var path = new[] { "properties", "a", "properties", "b" };
- var result = OpenApiWorkspace.ResolveSubSchema(schema, path);
+ var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []);
Assert.NotNull(result);
Assert.Equal(JsonSchemaType.String, result!.Type);
@@ -250,7 +250,7 @@ public void ResolveSubSchema_ShouldHandleUserDefinedKeywordNamedProperty(string[
}
};
- var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments);
+ var result = OpenApiWorkspace.ResolveSubSchema(schema, pathSegments, []);
Assert.NotNull(result);
Assert.Equal(JsonSchemaType.String, result!.Type);
@@ -275,7 +275,7 @@ public void ResolveSubSchema_ShouldRecurseIntoAllOfComposition()
var path = new[] { "allOf", "0", "properties", "x" };
- var result = OpenApiWorkspace.ResolveSubSchema(schema, path);
+ var result = OpenApiWorkspace.ResolveSubSchema(schema, path, []);
Assert.NotNull(result);
Assert.Equal(JsonSchemaType.Integer, result!.Type);
@@ -434,6 +434,81 @@ public async Task ShouldResolveReferencesInSchemasFromSystemTextJson()
var updatedTagsProperty = Assert.IsType(schema.Properties["tags"]);
Assert.Equal(absoluteReferenceId, updatedTagsProperty.Reference.ReferenceV3);
Assert.Equal(JsonSchemaType.Array | JsonSchemaType.Null, updatedTagsProperty.Type);
+ Assert.Equal(JsonSchemaType.Object, updatedTagsProperty.Items.Type);
+
+
+ // doing the same for the parent property
+
+ var parentProperty = Assert.IsType(schema.Properties["parent"]);
+ var parentSubProperty = Assert.IsType(parentProperty.Properties["parent"]);
+ Assert.Equal("#/properties/parent", parentSubProperty.Reference.ReferenceV3);
+ parentProperty.Properties["parent"] = new OpenApiSchemaReference($"#/components/schemas/Foo{parentSubProperty.Reference.ReferenceV3.Replace("#", string.Empty)}", document);
+ var updatedParentSubProperty = Assert.IsType(parentProperty.Properties["parent"]);
+ Assert.Equal(JsonSchemaType.Object | JsonSchemaType.Null, updatedParentSubProperty.Type);
+
+ var pathItem = new OpenApiPathItem
+ {
+ Operations = new Dictionary
+ {
+ [HttpMethod.Post] = new OpenApiOperation
+ {
+ Responses = new OpenApiResponses
+ {
+ ["200"] = new OpenApiResponse
+ {
+ }
+ },
+ RequestBody = new OpenApiRequestBody
+ {
+ Content = new Dictionary
+ {
+ ["application/json"] = new OpenApiMediaType
+ {
+ Schema = new OpenApiSchemaReference("#/components/schemas/Foo", document)
+ }
+ }
+ }
+ }
+ }
+ };
+ document.Paths.Add("/", pathItem);
+
+ var requestBodySchema = pathItem.Operations[HttpMethod.Post].RequestBody.Content["application/json"].Schema;
+ Assert.NotNull(requestBodySchema);
+ var requestBodyTagsProperty = Assert.IsType(requestBodySchema.Properties["tags"]);
+ Assert.Equal(JsonSchemaType.Object, requestBodyTagsProperty.Items.Type);
+ }
+
+ [Fact]
+ public void ExitsEarlyOnCyclicalReferences()
+ {
+ var document = new OpenApiDocument
+ {
+ Info = new OpenApiInfo { Title = "Test API", Version = "1.0.0" },
+ };
+ var categorySchema = new OpenApiSchema
+ {
+ Type = JsonSchemaType.Object,
+ Properties = new Dictionary
+ {
+ ["name"] = new OpenApiSchema { Type = JsonSchemaType.String },
+ ["parent"] = new OpenApiSchemaReference("#/components/schemas/Category", document),
+ // this is intentionally wrong and cyclical reference
+ // it tests whether we're going in an infinite resolution loop
+ ["tags"] = new OpenApiSchemaReference("#/components/schemas/Category/properties/parent/properties/tags", document)
+ }
+ };
+ document.AddComponent("Category", categorySchema);
+ document.RegisterComponents();
+
+ var tagsSchemaRef = Assert.IsType(categorySchema.Properties["tags"]);
+ Assert.Null(tagsSchemaRef.Items);
+ Assert.Equal("#/components/schemas/Category/properties/parent/properties/tags", tagsSchemaRef.Reference.ReferenceV3);
+ Assert.Null(tagsSchemaRef.Target);
+
+ var parentSchemaRef = Assert.IsType(categorySchema.Properties["parent"]);
+ Assert.Equal("#/components/schemas/Category", parentSchemaRef.Reference.ReferenceV3);
+ Assert.NotNull(parentSchemaRef.Target);
}
}
}