@@ -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