Skip to content

Commit 7ba4318

Browse files
committed
Resolve relative references before processing
1 parent f4e259a commit 7ba4318

File tree

5 files changed

+144
-12
lines changed

5 files changed

+144
-12
lines changed

src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,5 +457,111 @@ private async Task InnerApplySchemaTransformersAsync(IOpenApiSchema inputSchema,
457457
}
458458

459459
private JsonNode CreateSchema(OpenApiSchemaKey key)
460-
=> JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration);
460+
{
461+
var schema = JsonSchemaExporter.GetJsonSchemaAsNode(_jsonSerializerOptions, key.Type, _configuration);
462+
return ResolveReferences(schema, schema);
463+
}
464+
465+
/// <summary>
466+
/// Recursively resolves references within a JSON schema node.
467+
/// </summary>
468+
private static JsonNode ResolveReferences(JsonNode node, JsonNode rootSchema)
469+
{
470+
if (node is JsonObject jsonObject)
471+
{
472+
// Check if this is a reference object
473+
if (jsonObject.TryGetPropertyValue("$ref", out var refNode) &&
474+
refNode is JsonValue refValue &&
475+
refValue.TryGetValue<string>(out var refString) &&
476+
refString.StartsWith('#'))
477+
{
478+
// Resolve the reference path to the actual schema content
479+
var resolvedNode = ResolveReference(refString, rootSchema);
480+
if (resolvedNode != null)
481+
{
482+
// Return a deep clone to avoid parent issues
483+
return resolvedNode.DeepClone();
484+
}
485+
// If resolution fails, return the original reference
486+
return node;
487+
}
488+
489+
// Process all properties recursively
490+
var newObject = new JsonObject();
491+
foreach (var property in jsonObject)
492+
{
493+
if (property.Value != null)
494+
{
495+
var processedValue = ResolveReferences(property.Value, rootSchema);
496+
// Clone the processed value to avoid parent issues
497+
newObject[property.Key] = processedValue?.DeepClone();
498+
}
499+
else
500+
{
501+
newObject[property.Key] = null;
502+
}
503+
}
504+
return newObject;
505+
}
506+
else if (node is JsonArray jsonArray)
507+
{
508+
var newArray = new JsonArray();
509+
for (var i = 0; i < jsonArray.Count; i++)
510+
{
511+
if (jsonArray[i] != null)
512+
{
513+
var processedValue = ResolveReferences(jsonArray[i]!, rootSchema);
514+
// Clone the processed value to avoid parent issues
515+
newArray.Add(processedValue?.DeepClone());
516+
}
517+
else
518+
{
519+
newArray.Add(null);
520+
}
521+
}
522+
return newArray;
523+
}
524+
525+
// Return primitive values as-is
526+
return node;
527+
}
528+
529+
/// <summary>
530+
/// Resolves a JSON reference path (like "#/properties/parent/properties/tags") to the actual schema content.
531+
/// </summary>
532+
private static JsonNode? ResolveReference(string refPath, JsonNode rootSchema)
533+
{
534+
// Remove the leading "#" and split the path
535+
var path = refPath.TrimStart('#').TrimStart('/');
536+
if (string.IsNullOrEmpty(path))
537+
{
538+
return rootSchema;
539+
}
540+
541+
var segments = path.Split('/');
542+
var current = rootSchema;
543+
544+
foreach (var segment in segments)
545+
{
546+
if (current is JsonObject currentObject)
547+
{
548+
if (currentObject.TryGetPropertyValue(segment, out var nextNode) && nextNode != null)
549+
{
550+
current = nextNode;
551+
}
552+
else
553+
{
554+
// Path not found
555+
return null;
556+
}
557+
}
558+
else
559+
{
560+
// Cannot navigate further
561+
return null;
562+
}
563+
}
564+
565+
return current;
566+
}
461567
}

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_0/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,10 @@
611611
"$ref": "#/components/schemas/Category"
612612
},
613613
"tags": {
614-
"$ref": "#/components/schemas/Category/properties/parent/properties/tags"
614+
"type": "array",
615+
"items": {
616+
"$ref": "#/components/schemas/Tag"
617+
}
615618
}
616619
}
617620
},
@@ -645,7 +648,10 @@
645648
"seq2": {
646649
"type": "array",
647650
"items": {
648-
"$ref": "#/components/schemas/ContainerType/properties/seq1/items"
651+
"type": "array",
652+
"items": {
653+
"type": "string"
654+
}
649655
}
650656
}
651657
}
@@ -665,7 +671,10 @@
665671
"type": "object",
666672
"properties": {
667673
"name": {
668-
"$ref": "#/components/schemas/Root/properties/item1/properties/name"
674+
"type": "array",
675+
"items": {
676+
"type": "string"
677+
}
669678
},
670679
"value": {
671680
"type": "integer",

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApi3_1/OpenApiDocumentIntegrationTests.VerifyOpenApiDocument_documentName=schemas-by-ref.verified.txt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,10 @@
611611
"$ref": "#/components/schemas/Category"
612612
},
613613
"tags": {
614-
"$ref": "#/components/schemas/Category/properties/parent/properties/tags"
614+
"type": "array",
615+
"items": {
616+
"$ref": "#/components/schemas/Tag"
617+
}
615618
}
616619
}
617620
},
@@ -645,7 +648,10 @@
645648
"seq2": {
646649
"type": "array",
647650
"items": {
648-
"$ref": "#/components/schemas/ContainerType/properties/seq1/items"
651+
"type": "array",
652+
"items": {
653+
"type": "string"
654+
}
649655
}
650656
}
651657
}
@@ -665,7 +671,10 @@
665671
"type": "object",
666672
"properties": {
667673
"name": {
668-
"$ref": "#/components/schemas/Root/properties/item1/properties/name"
674+
"type": "array",
675+
"items": {
676+
"type": "string"
677+
}
669678
},
670679
"value": {
671680
"type": "integer",

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Integration/snapshots/OpenApiDocumentLocalizationTests.VerifyOpenApiDocumentIsInvariant.verified.txt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,10 @@
13781378
"$ref": "#/components/schemas/Category"
13791379
},
13801380
"tags": {
1381-
"$ref": "#/components/schemas/Category/properties/parent/properties/tags"
1381+
"type": "array",
1382+
"items": {
1383+
"$ref": "#/components/schemas/Tag"
1384+
}
13821385
}
13831386
}
13841387
},
@@ -1412,7 +1415,10 @@
14121415
"seq2": {
14131416
"type": "array",
14141417
"items": {
1415-
"$ref": "#/components/schemas/ContainerType/properties/seq1/items"
1418+
"type": "array",
1419+
"items": {
1420+
"type": "string"
1421+
}
14161422
}
14171423
}
14181424
}
@@ -1454,7 +1460,10 @@
14541460
"type": "object",
14551461
"properties": {
14561462
"name": {
1457-
"$ref": "#/components/schemas/Root/properties/item1/properties/name"
1463+
"type": "array",
1464+
"items": {
1465+
"type": "string"
1466+
}
14581467
},
14591468
"value": {
14601469
"type": "integer",

src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Transformers/Implementations/OpenApiSchemaReferenceTransformerTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -740,8 +740,7 @@ await VerifyOpenApiDocument(builder, document =>
740740
Assert.Equal("Category", ((OpenApiSchemaReference)requestSchema).Reference.Id);
741741

742742
// Assert that $ref is used for nested Tags
743-
// Todo: See https://github.com/microsoft/OpenAPI.NET/issues/2062
744-
// Assert.Equal("Tag", ((OpenApiSchemaReference)requestSchema.Properties["tags"].Items).Reference.Id);
743+
Assert.Equal("Tag", ((OpenApiSchemaReference)requestSchema.Properties["tags"].Items).Reference.Id);
745744

746745
// Assert that $ref is used for nested Parent
747746
Assert.Equal("Category", ((OpenApiSchemaReference)requestSchema.Properties["parent"]).Reference.Id);

0 commit comments

Comments
 (0)