Skip to content

Commit fb50e52

Browse files
committed
Clean up implementation
1 parent bd036c4 commit fb50e52

File tree

2 files changed

+54
-25
lines changed

2 files changed

+54
-25
lines changed

src/OpenApi/src/Services/OpenApiConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ internal static class OpenApiConstants
1515
internal const string RefId = "x-ref-id";
1616
internal const string RefDescriptionAnnotation = "x-ref-description";
1717
internal const string RefExampleAnnotation = "x-ref-example";
18+
internal const string RefKeyword = "$ref";
19+
internal const string RefPrefix = "#";
1820
internal const string DefaultOpenApiResponseKey = "default";
1921
// Since there's a finite set of HTTP methods that can be included in a given
2022
// OpenApiPaths, we can pre-allocate an array of these methods and use a direct

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

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -462,26 +462,48 @@ private JsonNode CreateSchema(OpenApiSchemaKey key)
462462
return ResolveReferences(schema, schema);
463463
}
464464

465-
/// <summary>
466-
/// Recursively resolves references within a JSON schema node.
467-
/// </summary>
468465
private static JsonNode ResolveReferences(JsonNode node, JsonNode rootSchema)
466+
{
467+
return ResolveReferencesRecursive(node, rootSchema, []);
468+
}
469+
470+
private static JsonNode ResolveReferencesRecursive(JsonNode node, JsonNode rootSchema, HashSet<string> visitedRefs)
469471
{
470472
if (node is JsonObject jsonObject)
471473
{
472-
// Check if this is a reference object
473-
if (jsonObject.TryGetPropertyValue("$ref", out var refNode) &&
474+
if (jsonObject.TryGetPropertyValue(OpenApiConstants.RefKeyword, out var refNode) &&
474475
refNode is JsonValue refValue &&
475476
refValue.TryGetValue<string>(out var refString) &&
476-
refString.StartsWith('#'))
477+
refString.StartsWith(OpenApiConstants.RefPrefix, StringComparison.Ordinal))
477478
{
478-
// Resolve the reference path to the actual schema content
479-
var resolvedNode = ResolveReference(refString, rootSchema);
480-
if (resolvedNode != null)
479+
if (visitedRefs.Contains(refString))
480+
{
481+
return node;
482+
}
483+
484+
visitedRefs.Add(refString);
485+
486+
try
487+
{
488+
// Resolve the reference path to the actual schema content
489+
// to avoid relative references
490+
var resolvedNode = ResolveReference(refString, rootSchema);
491+
if (resolvedNode != null)
492+
{
493+
return resolvedNode.DeepClone();
494+
}
495+
}
496+
catch (InvalidOperationException)
497+
{
498+
// If resolution fails due to invalid path, return the original reference
499+
// This maintains backward compatibility while preventing crashes
500+
}
501+
finally
481502
{
482-
// Return a deep clone to avoid parent issues
483-
return resolvedNode.DeepClone();
503+
// Remove from visited set to allow the same reference in different branches
504+
visitedRefs.Remove(refString);
484505
}
506+
485507
// If resolution fails, return the original reference
486508
return node;
487509
}
@@ -492,8 +514,7 @@ refNode is JsonValue refValue &&
492514
{
493515
if (property.Value != null)
494516
{
495-
var processedValue = ResolveReferences(property.Value, rootSchema);
496-
// Clone the processed value to avoid parent issues
517+
var processedValue = ResolveReferencesRecursive(property.Value, rootSchema, visitedRefs);
497518
newObject[property.Key] = processedValue?.DeepClone();
498519
}
499520
else
@@ -510,8 +531,7 @@ refNode is JsonValue refValue &&
510531
{
511532
if (jsonArray[i] != null)
512533
{
513-
var processedValue = ResolveReferences(jsonArray[i]!, rootSchema);
514-
// Clone the processed value to avoid parent issues
534+
var processedValue = ResolveReferencesRecursive(jsonArray[i]!, rootSchema, visitedRefs);
515535
newArray.Add(processedValue?.DeepClone());
516536
}
517537
else
@@ -522,16 +542,22 @@ refNode is JsonValue refValue &&
522542
return newArray;
523543
}
524544

525-
// Return primitive values as-is
545+
// Return non-$ref nodes as-is
526546
return node;
527547
}
528548

529-
/// <summary>
530-
/// Resolves a JSON reference path (like "#/properties/parent/properties/tags") to the actual schema content.
531-
/// </summary>
532549
private static JsonNode? ResolveReference(string refPath, JsonNode rootSchema)
533550
{
534-
// Remove the leading "#" and split the path
551+
if (string.IsNullOrWhiteSpace(refPath))
552+
{
553+
throw new InvalidOperationException("Reference path cannot be null or empty.");
554+
}
555+
556+
if (!refPath.StartsWith(OpenApiConstants.RefPrefix, StringComparison.Ordinal))
557+
{
558+
throw new InvalidOperationException($"Only fragment references (starting with '{OpenApiConstants.RefPrefix}') are supported. Found: {refPath}");
559+
}
560+
535561
var path = refPath.TrimStart('#').TrimStart('/');
536562
if (string.IsNullOrEmpty(path))
537563
{
@@ -541,8 +567,9 @@ refNode is JsonValue refValue &&
541567
var segments = path.Split('/');
542568
var current = rootSchema;
543569

544-
foreach (var segment in segments)
570+
for (var i = 0; i < segments.Length; i++)
545571
{
572+
var segment = segments[i];
546573
if (current is JsonObject currentObject)
547574
{
548575
if (currentObject.TryGetPropertyValue(segment, out var nextNode) && nextNode != null)
@@ -551,14 +578,14 @@ refNode is JsonValue refValue &&
551578
}
552579
else
553580
{
554-
// Path not found
555-
return null;
581+
var partialPath = string.Join("/", segments.Take(i + 1));
582+
throw new InvalidOperationException($"Failed to resolve reference '{refPath}': path segment '{segment}' not found at '#{partialPath}'");
556583
}
557584
}
558585
else
559586
{
560-
// Cannot navigate further
561-
return null;
587+
var partialPath = string.Join("/", segments.Take(i));
588+
throw new InvalidOperationException($"Failed to resolve reference '{refPath}': cannot navigate beyond '#{partialPath}' - expected object but found {current?.GetType().Name ?? "null"}");
562589
}
563590
}
564591

0 commit comments

Comments
 (0)