Skip to content

Commit 296c7d4

Browse files
committed
tests pass
1 parent 2f0cc1a commit 296c7d4

1 file changed

Lines changed: 65 additions & 13 deletions

File tree

  • json-java21-schema/src/main/java/io/github/simbo1905/json/schema

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java

Lines changed: 65 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)