@@ -124,10 +124,16 @@ default ValidationResult validate(JsonValue json) {
124124 Objects .requireNonNull (json , "json" );
125125 List <ValidationError > errors = new ArrayList <>();
126126 Deque <ValidationFrame > stack = new ArrayDeque <>();
127+ Set <ValidationKey > visited = new HashSet <>();
127128 stack .push (new ValidationFrame ("" , this , json ));
128129
129130 while (!stack .isEmpty ()) {
130131 ValidationFrame frame = stack .pop ();
132+ ValidationKey key = new ValidationKey (frame .schema (), frame .json (), frame .path ());
133+ if (!visited .add (key )) {
134+ LOG .finest (() -> "SKIP " + frame .path () + " schema=" + frame .schema ().getClass ().getSimpleName ());
135+ continue ;
136+ }
131137 LOG .finest (() -> "POP " + frame .path () +
132138 " schema=" + frame .schema ().getClass ().getSimpleName ());
133139 ValidationResult result = frame .schema .validateAt (frame .path , frame .json , stack );
@@ -719,6 +725,40 @@ record ValidationError(String path, String message) {}
719725 /// Validation frame for stack-based processing
720726 record ValidationFrame (String path , JsonSchema schema , JsonValue json ) {}
721727
728+ /// Internal key used to detect and break validation cycles
729+ final class ValidationKey {
730+ private final JsonSchema schema ;
731+ private final JsonValue json ;
732+ private final String path ;
733+
734+ ValidationKey (JsonSchema schema , JsonValue json , String path ) {
735+ this .schema = schema ;
736+ this .json = json ;
737+ this .path = path ;
738+ }
739+
740+ @ Override
741+ public boolean equals (Object obj ) {
742+ if (this == obj ) {
743+ return true ;
744+ }
745+ if (!(obj instanceof ValidationKey other )) {
746+ return false ;
747+ }
748+ return this .schema == other .schema &&
749+ this .json == other .json &&
750+ Objects .equals (this .path , other .path );
751+ }
752+
753+ @ Override
754+ public int hashCode () {
755+ int result = System .identityHashCode (schema );
756+ result = 31 * result + System .identityHashCode (json );
757+ result = 31 * result + (path != null ? path .hashCode () : 0 );
758+ return result ;
759+ }
760+ }
761+
722762 /// Canonicalization helper for structural equality in uniqueItems
723763 private static String canonicalize (JsonValue v ) {
724764 if (v instanceof JsonObject o ) {
@@ -1155,24 +1195,36 @@ private static JsonSchema compileInternalWithContext(JsonValue schemaJson, java.
11551195 throw new IllegalArgumentException ("Cyclic $ref: " + String .join (" -> " , resolutionStack ) + " -> " + pointer );
11561196 }
11571197
1158- // Push to resolution stack for cycle detection
1159- resolutionStack .push (pointer );
1160- try {
1161- // Try to get from local pointer index first (for already compiled definitions)
1162- JsonSchema cached = localPointerIndex .get (pointer );
1163- if (cached != null ) {
1164- return cached ;
1198+ // Try to get from local pointer index first (for already compiled definitions)
1199+ JsonSchema cached = localPointerIndex .get (pointer );
1200+ if (cached != null ) {
1201+ return cached ;
1202+ }
1203+
1204+ // Otherwise, resolve via JSON Pointer and compile
1205+ Optional <JsonValue > target = navigatePointer (rawByPointer .get ("" ), pointer );
1206+ if (target .isPresent ()) {
1207+ // Check if the target itself contains a $ref that would create a cycle
1208+ JsonValue targetValue = target .get ();
1209+ if (targetValue instanceof JsonObject targetObj ) {
1210+ JsonValue targetRef = targetObj .members ().get ("$ref" );
1211+ if (targetRef instanceof JsonString targetRefStr ) {
1212+ String targetRefPointer = targetRefStr .value ();
1213+ if (resolutionStack .contains (targetRefPointer )) {
1214+ throw new IllegalArgumentException ("Cyclic $ref: " + String .join (" -> " , resolutionStack ) + " -> " + pointer + " -> " + targetRefPointer );
1215+ }
1216+ }
11651217 }
11661218
1167- // Otherwise, resolve via JSON Pointer and compile
1168- Optional < JsonValue > target = navigatePointer ( rawByPointer . get ( "" ), pointer );
1169- if ( target . isPresent ()) {
1170- JsonSchema compiled = compileInternalWithContext (target . get () , docUri , workStack , seenUris , resolverContext , localPointerIndex , resolutionStack );
1219+ // Push to resolution stack for cycle detection before compiling
1220+ resolutionStack . push ( pointer );
1221+ try {
1222+ JsonSchema compiled = compileInternalWithContext (targetValue , docUri , workStack , seenUris , resolverContext , localPointerIndex , resolutionStack );
11711223 localPointerIndex .put (pointer , compiled );
11721224 return compiled ;
1225+ } finally {
1226+ resolutionStack .pop ();
11731227 }
1174- } finally {
1175- resolutionStack .pop ();
11761228 }
11771229 }
11781230
0 commit comments