@@ -516,10 +516,14 @@ public ValidationResult validateAt(String path, JsonValue json, Deque<Validation
516516 }
517517
518518 /// Reference schema for JSON Schema $ref
519- record RefSchema (String ref ) implements JsonSchema {
519+ record RefSchema (String ref , java . util . function . Supplier < JsonSchema > targetSupplier ) implements JsonSchema {
520520 @ Override
521521 public ValidationResult validateAt (String path , JsonValue json , Deque <ValidationFrame > stack ) {
522- throw new UnsupportedOperationException ("$ref resolution not implemented" );
522+ JsonSchema target = targetSupplier .get ();
523+ if (target == null ) {
524+ return ValidationResult .failure (List .of (new ValidationError (path , "Unresolved $ref: " + ref )));
525+ }
526+ return target .validateAt (path , json , stack );
523527 }
524528 }
525529
@@ -757,6 +761,9 @@ final class SchemaCompiler {
757761 private static final Map <String , JsonSchema > definitions = new HashMap <>();
758762 private static JsonSchema currentRootSchema ;
759763 private static Options currentOptions ;
764+ private static final Map <String , JsonSchema > compiledByPointer = new HashMap <>();
765+ private static final Map <String , JsonValue > rawByPointer = new HashMap <>();
766+ private static final Deque <String > resolutionStack = new ArrayDeque <>();
760767
761768 private static void trace (String stage , JsonValue fragment ) {
762769 if (LOG .isLoggable (Level .FINER )) {
@@ -765,12 +772,124 @@ private static void trace(String stage, JsonValue fragment) {
765772 }
766773 }
767774
775+ /// JSON Pointer utility for RFC-6901 fragment navigation
776+ static Optional <JsonValue > navigatePointer (JsonValue root , String pointer ) {
777+ if (pointer .isEmpty () || pointer .equals ("#" )) {
778+ return Optional .of (root );
779+ }
780+
781+ // Remove leading # if present
782+ String path = pointer .startsWith ("#" ) ? pointer .substring (1 ) : pointer ;
783+ if (path .isEmpty ()) {
784+ return Optional .of (root );
785+ }
786+
787+ // Must start with /
788+ if (!path .startsWith ("/" )) {
789+ return Optional .empty ();
790+ }
791+
792+ JsonValue current = root ;
793+ String [] tokens = path .substring (1 ).split ("/" );
794+
795+ for (String token : tokens ) {
796+ // Unescape ~1 -> / and ~0 -> ~
797+ String unescaped = token .replace ("~1" , "/" ).replace ("~0" , "~" );
798+
799+ if (current instanceof JsonObject obj ) {
800+ current = obj .members ().get (unescaped );
801+ if (current == null ) {
802+ return Optional .empty ();
803+ }
804+ } else if (current instanceof JsonArray arr ) {
805+ try {
806+ int index = Integer .parseInt (unescaped );
807+ if (index < 0 || index >= arr .values ().size ()) {
808+ return Optional .empty ();
809+ }
810+ current = arr .values ().get (index );
811+ } catch (NumberFormatException e ) {
812+ return Optional .empty ();
813+ }
814+ } else {
815+ return Optional .empty ();
816+ }
817+ }
818+
819+ return Optional .of (current );
820+ }
821+
822+ /// Resolve $ref with cycle detection and memoization
823+ static JsonSchema resolveRef (String ref ) {
824+ // Check for cycles
825+ if (resolutionStack .contains (ref )) {
826+ throw new IllegalArgumentException ("Cyclic $ref: " + String .join (" -> " , resolutionStack ) + " -> " + ref );
827+ }
828+
829+ // Check memoized results
830+ JsonSchema cached = compiledByPointer .get (ref );
831+ if (cached != null ) {
832+ return cached ;
833+ }
834+
835+ if (ref .equals ("#" )) {
836+ // Root reference - return RootRef instead of RefSchema to avoid cycles
837+ return new RootRef (() -> currentRootSchema );
838+ }
839+
840+ // Resolve via JSON Pointer
841+ Optional <JsonValue > target = navigatePointer (rawByPointer .get ("" ), ref );
842+ if (target .isEmpty ()) {
843+ throw new IllegalArgumentException ("Unresolved $ref: " + ref );
844+ }
845+
846+ // Check if it's a boolean schema
847+ JsonValue targetValue = target .get ();
848+ if (targetValue instanceof JsonBoolean bool ) {
849+ JsonSchema schema = bool .value () ? AnySchema .INSTANCE : new NotSchema (AnySchema .INSTANCE );
850+ compiledByPointer .put (ref , schema );
851+ return new RefSchema (ref , () -> schema );
852+ }
853+
854+ // Push to resolution stack for cycle detection
855+ resolutionStack .push (ref );
856+ try {
857+ JsonSchema compiled = compileInternal (targetValue );
858+ compiledByPointer .put (ref , compiled );
859+ final JsonSchema finalCompiled = compiled ;
860+ return new RefSchema (ref , () -> finalCompiled );
861+ } finally {
862+ resolutionStack .pop ();
863+ }
864+ }
865+
866+ /// Index schema fragments by JSON Pointer for efficient lookup
867+ static void indexSchemaByPointer (String pointer , JsonValue value ) {
868+ rawByPointer .put (pointer , value );
869+
870+ if (value instanceof JsonObject obj ) {
871+ for (var entry : obj .members ().entrySet ()) {
872+ String key = entry .getKey ();
873+ // Escape special characters in key
874+ String escapedKey = key .replace ("~" , "~0" ).replace ("/" , "~1" );
875+ indexSchemaByPointer (pointer + "/" + escapedKey , entry .getValue ());
876+ }
877+ } else if (value instanceof JsonArray arr ) {
878+ for (int i = 0 ; i < arr .values ().size (); i ++) {
879+ indexSchemaByPointer (pointer + "/" + i , arr .values ().get (i ));
880+ }
881+ }
882+ }
883+
768884 static JsonSchema compile (JsonValue schemaJson ) {
769885 return compile (schemaJson , Options .DEFAULT );
770886 }
771887
772888 static JsonSchema compile (JsonValue schemaJson , Options options ) {
773889 definitions .clear (); // Clear any previous definitions
890+ compiledByPointer .clear ();
891+ rawByPointer .clear ();
892+ resolutionStack .clear ();
774893 currentRootSchema = null ;
775894 currentOptions = options ;
776895
@@ -794,6 +913,9 @@ static JsonSchema compile(JsonValue schemaJson, Options options) {
794913 // Update options with final assertion setting
795914 currentOptions = new Options (assertFormats );
796915
916+ // Index the raw schema by JSON Pointer
917+ indexSchemaByPointer ("" , schemaJson );
918+
797919 trace ("compile-start" , schemaJson );
798920 JsonSchema schema = compileInternal (schemaJson );
799921 currentRootSchema = schema ; // Store the root schema for self-references
@@ -814,7 +936,10 @@ private static JsonSchema compileInternal(JsonValue schemaJson) {
814936 if (defsValue instanceof JsonObject defsObj ) {
815937 trace ("compile-defs" , defsValue );
816938 for (var entry : defsObj .members ().entrySet ()) {
817- definitions .put ("#/$defs/" + entry .getKey (), compileInternal (entry .getValue ()));
939+ String pointer = "#/$defs/" + entry .getKey ();
940+ JsonSchema compiled = compileInternal (entry .getValue ());
941+ definitions .put (pointer , compiled );
942+ compiledByPointer .put (pointer , compiled );
818943 }
819944 }
820945
@@ -823,15 +948,7 @@ private static JsonSchema compileInternal(JsonValue schemaJson) {
823948 if (refValue instanceof JsonString refStr ) {
824949 String ref = refStr .value ();
825950 trace ("compile-ref" , refValue );
826- if (ref .equals ("#" )) {
827- // Lazily resolve to whatever the root schema becomes after compilation
828- return new RootRef (() -> currentRootSchema );
829- }
830- JsonSchema resolved = definitions .get (ref );
831- if (resolved == null ) {
832- throw new IllegalArgumentException ("Unresolved $ref: " + ref );
833- }
834- return resolved ;
951+ return resolveRef (ref );
835952 }
836953
837954 // Handle composition keywords
@@ -1270,14 +1387,30 @@ public ValidationResult validateAt(String path, JsonValue json, Deque<Validation
12701387
12711388 /// Root reference schema that refers back to the root schema
12721389 record RootRef (java .util .function .Supplier <JsonSchema > rootSupplier ) implements JsonSchema {
1390+ // Track recursion depth per thread to avoid infinite loops
1391+ private static final ThreadLocal <Integer > recursionDepth = ThreadLocal .withInitial (() -> 0 );
1392+ private static final int MAX_RECURSION_DEPTH = 50 ;
1393+
12731394 @ Override
12741395 public ValidationResult validateAt (String path , JsonValue json , Deque <ValidationFrame > stack ) {
12751396 JsonSchema root = rootSupplier .get ();
12761397 if (root == null ) {
12771398 // No root yet (should not happen during validation), accept for now
12781399 return ValidationResult .success ();
12791400 }
1280- return root .validate (json ); // Direct validation against root schema
1401+
1402+ // Check recursion depth to prevent infinite loops
1403+ int depth = recursionDepth .get ();
1404+ if (depth >= MAX_RECURSION_DEPTH ) {
1405+ return ValidationResult .success (); // Break the cycle
1406+ }
1407+
1408+ try {
1409+ recursionDepth .set (depth + 1 );
1410+ return root .validate (json );
1411+ } finally {
1412+ recursionDepth .set (depth );
1413+ }
12811414 }
12821415 }
12831416
0 commit comments