diff --git a/README.md b/README.md index 5c3a493..678d82a 100644 --- a/README.md +++ b/README.md @@ -90,11 +90,11 @@ This is a simplified backport with the following changes from the original: These vulnerabilities exist in the upstream OpenJDK sandbox implementation and are reported here for transparency. -## JSON Schema Validator (2020-12) +## JSON Schema Validator By including a basic schema validator that demonstrates how to build a realistic feature out of the core API. To demonstrate the power of the core API, it follows Data Oriented Programming principles: it parses JSON Schema into an immutable structure of records, then for validation it parses the JSON to the generic structure and uses the thread-safe parsed schema as the model to validate the JSON being checked. -A simple JSON Schema (2020-12 subset) validator is included (module: json-java21-schema). +A simple JSON Schema validator is included (module: json-java21-schema). ```java var schema = io.github.simbo1905.json.schema.JsonSchema.compile( @@ -328,12 +328,9 @@ This backport includes a compatibility report tool that tests against the [JSON ### Running the Compatibility Report -First, build the project and download the test suite: +The test data is bundled as ZIP files and extracted automatically at runtime: ```bash -# Build project and download test suite -mvn clean compile generate-test-resources -pl json-compatibility-suite - # Run human-readable report mvn exec:java -pl json-compatibility-suite diff --git a/json-compatibility-suite/pom.xml b/json-compatibility-suite/pom.xml index 342f8e2..338522f 100644 --- a/json-compatibility-suite/pom.xml +++ b/json-compatibility-suite/pom.xml @@ -50,30 +50,30 @@ - com.googlecode.maven-download-plugin - download-maven-plugin - - - download-json-test-suite - pre-integration-test - - wget - - - https://github.com/nst/JSONTestSuite/archive/refs/heads/master.zip - ${project.build.directory}/test-resources - json-test-suite.zip - true - - - + org.apache.maven.plugins + maven-compiler-plugin + + + src/test/resources/**/*.java + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/JSONTestSuite-20250921/**/*.java + **/parsers/**/*.java + + org.codehaus.mojo exec-maven-plugin 3.4.1 - jdk.sandbox.compatibility.JsonTestSuiteSummary + jdk.sandbox.compatibility.JsonCompatibilitySummary false diff --git a/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java b/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonCompatibilitySummary.java similarity index 78% rename from json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java rename to json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonCompatibilitySummary.java index b53f295..3661454 100644 --- a/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonTestSuiteSummary.java +++ b/json-compatibility-suite/src/main/java/jdk/sandbox/compatibility/JsonCompatibilitySummary.java @@ -15,17 +15,42 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; /// Generates a conformance summary report. /// Run with: mvn exec:java -pl json-compatibility-suite -public class JsonTestSuiteSummary { +/// Test data location: see src/test/resources/json-test-suite-data.zip +public class JsonCompatibilitySummary { - private static final Logger LOGGER = Logger.getLogger(JsonTestSuiteSummary.class.getName()); - private static final Path TEST_DIR = Paths.get("json-compatibility-suite/target/test-resources/JSONTestSuite-master/test_parsing"); + private static final Logger LOGGER = Logger.getLogger(JsonCompatibilitySummary.class.getName()); + private static final Path ZIP_FILE = findZipFile(); + private static final Path TARGET_TEST_DIR = Paths.get("target/test-data/json-test-suite/test_parsing"); + + private static Path findZipFile() { + // Try different possible locations for the ZIP file + Path[] candidates = { + Paths.get("src/test/resources/json-test-suite-data.zip"), + Paths.get("json-compatibility-suite/src/test/resources/json-test-suite-data.zip"), + Paths.get("../json-compatibility-suite/src/test/resources/json-test-suite-data.zip") + }; + + for (Path candidate : candidates) { + if (Files.exists(candidate)) { + return candidate; + } + } + + // If none found, return the first candidate and let it fail with a clear message + return candidates[0]; + } public static void main(String[] args) throws Exception { boolean jsonOutput = args.length > 0 && "--json".equals(args[0]); - JsonTestSuiteSummary summary = new JsonTestSuiteSummary(); + JsonCompatibilitySummary summary = new JsonCompatibilitySummary(); + summary.extractTestData(); if (jsonOutput) { summary.generateJsonReport(); } else { @@ -33,6 +58,28 @@ public static void main(String[] args) throws Exception { } } + void extractTestData() throws IOException { + if (!Files.exists(ZIP_FILE)) { + throw new RuntimeException("Test data ZIP file not found: " + ZIP_FILE.toAbsolutePath()); + } + + // Create target directory + Files.createDirectories(TARGET_TEST_DIR.getParent()); + + // Extract ZIP file + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(ZIP_FILE.toFile()))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (!entry.isDirectory() && entry.getName().startsWith("test_parsing/")) { + Path outputPath = TARGET_TEST_DIR.getParent().resolve(entry.getName()); + Files.createDirectories(outputPath.getParent()); + Files.copy(zis, outputPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + zis.closeEntry(); + } + } + } + void generateConformanceReport() throws Exception { LOGGER.fine(() -> "Starting conformance report generation"); TestResults results = runTests(); @@ -84,9 +131,9 @@ void generateJsonReport() throws Exception { } private TestResults runTests() throws Exception { - LOGGER.fine(() -> "Walking test files under: " + TEST_DIR.toAbsolutePath()); - if (!Files.exists(TEST_DIR)) { - throw new RuntimeException("Test suite not downloaded. Run: ./mvnw clean compile generate-test-resources -pl json-compatibility-suite"); + LOGGER.fine(() -> "Walking test files under: " + TARGET_TEST_DIR.toAbsolutePath()); + if (!Files.exists(TARGET_TEST_DIR)) { + throw new RuntimeException("Test data not extracted. Run extractTestData() first."); } List shouldPassButFailed = new ArrayList<>(); @@ -98,7 +145,7 @@ private TestResults runTests() throws Exception { int iAccept = 0, iReject = 0; List files; - try (var stream = Files.walk(TEST_DIR)) { + try (var stream = Files.walk(TARGET_TEST_DIR)) { files = stream .filter(p -> p.toString().endsWith(".json")) .sorted() diff --git a/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/DownloadVerificationTest.java b/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/DownloadVerificationTest.java index 2a9a176..5edede1 100644 --- a/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/DownloadVerificationTest.java +++ b/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/DownloadVerificationTest.java @@ -9,15 +9,26 @@ public class DownloadVerificationTest { @Test void testSuiteDownloaded() { - Path testDir = Paths.get("target/test-resources/JSONTestSuite-master/test_parsing"); - assertThat(testDir) - .as("JSON Test Suite should be downloaded and extracted") - .exists() - .isDirectory(); - - // Verify some test files exist - assertThat(testDir.resolve("y_structure_whitespace_array.json")) - .as("Should contain valid test files") - .exists(); + // The test data is now extracted from ZIP at runtime + // Create a summary instance and extract the data manually for testing + try { + JsonCompatibilitySummary summary = new JsonCompatibilitySummary(); + summary.extractTestData(); + + // Verify the target directory exists after extraction + Path targetDir = Paths.get("target/test-data/json-test-suite/test_parsing"); + assertThat(targetDir) + .as("JSON Test Suite should be extracted to target directory") + .exists() + .isDirectory(); + + // Verify some test files exist + assertThat(targetDir.resolve("y_valid_sample.json")) + .as("Should contain valid test files") + .exists(); + + } catch (Exception e) { + throw new RuntimeException("Failed to extract JSON test suite data", e); + } } } diff --git a/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/JsonTestSuiteTest.java b/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/JsonTestSuiteTest.java deleted file mode 100644 index 88a04bc..0000000 --- a/json-compatibility-suite/src/test/java/jdk/sandbox/compatibility/JsonTestSuiteTest.java +++ /dev/null @@ -1,138 +0,0 @@ -package jdk.sandbox.compatibility; - -import jdk.sandbox.java.util.json.Json; -import jdk.sandbox.java.util.json.JsonParseException; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.TestFactory; - -import java.nio.charset.MalformedInputException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.logging.Logger; -import java.util.stream.Stream; - -import static org.assertj.core.api.Assertions.*; - -/// Runs the JSON Test Suite against our implementation. -/// Files are categorized: -/// - y_*.json: Valid JSON that MUST parse successfully -/// - n_*.json: Invalid JSON that MUST fail to parse -/// - i_*.json: Implementation-defined (may accept or reject) -public class JsonTestSuiteTest { - - private static final Logger LOGGER = Logger.getLogger(JsonTestSuiteTest.class.getName()); - private static final Path TEST_DIR = Paths.get("target/test-resources/JSONTestSuite-master/test_parsing"); - - @TestFactory - @Disabled("This is now a reporting tool, not a blocking test. Use JsonTestSuiteSummary instead.") - Stream runJsonTestSuite() throws Exception { - if (!Files.exists(TEST_DIR)) { - System.err.println("Test suite not found. Run: mvn test-compile"); - return Stream.empty(); - } - - return Files.walk(TEST_DIR) - .filter(p -> p.toString().endsWith(".json")) - .sorted() - .map(this::createTest); - } - - private DynamicTest createTest(Path file) { - String filename = file.getFileName().toString(); - - return DynamicTest.dynamicTest(filename, () -> { - String content = null; - char[] charContent = null; - - try { - content = Files.readString(file, StandardCharsets.UTF_8); - charContent = content.toCharArray(); - } catch (MalformedInputException e) { - LOGGER.warning("UTF-8 failed for " + filename + ", using robust encoding detection"); - try { - byte[] rawBytes = Files.readAllBytes(file); - charContent = RobustCharDecoder.decodeToChars(rawBytes, filename); - } catch (Exception ex) { - throw new RuntimeException("Failed to read test file " + filename + " - this is a fundamental I/O failure, not an encoding issue: " + ex.getMessage(), ex); - } - } - - if (filename.startsWith("y_")) { - // Valid JSON - must parse successfully - testValidJson(filename, content, charContent); - - } else if (filename.startsWith("n_")) { - // Invalid JSON - must fail to parse - testInvalidJson(filename, content, charContent); - - } else if (filename.startsWith("i_")) { - // Implementation defined - just verify no crash - testImplementationDefinedJson(filename, content, charContent); - } - }); - } - - private void testValidJson(String filename, String content, char[] charContent) { - // Test String API if content is available - if (content != null) { - assertThatCode(() -> Json.parse(content)) - .as("File %s should parse successfully with String API", filename) - .doesNotThrowAnyException(); - } - - // Test char[] API - assertThatCode(() -> Json.parse(charContent)) - .as("File %s should parse successfully with char[] API", filename) - .doesNotThrowAnyException(); - } - - private void testInvalidJson(String filename, String content, char[] charContent) { - // Test String API if content is available - if (content != null) { - assertThatThrownBy(() -> Json.parse(content)) - .as("File %s should fail to parse with String API", filename) - .satisfiesAnyOf( - e -> assertThat(e).isInstanceOf(JsonParseException.class), - e -> assertThat(e).isInstanceOf(StackOverflowError.class) - .describedAs("StackOverflowError is acceptable for deeply nested structures like " + filename) - ); - } - - // Test char[] API - assertThatThrownBy(() -> Json.parse(charContent)) - .as("File %s should fail to parse with char[] API", filename) - .satisfiesAnyOf( - e -> assertThat(e).isInstanceOf(JsonParseException.class), - e -> assertThat(e).isInstanceOf(StackOverflowError.class) - .describedAs("StackOverflowError is acceptable for deeply nested structures like " + filename) - ); - } - - private void testImplementationDefinedJson(String filename, String content, char[] charContent) { - // Test String API if content is available - if (content != null) { - testImplementationDefinedSingle(filename + " (String API)", () -> Json.parse(content)); - } - - // Test char[] API - testImplementationDefinedSingle(filename + " (char[] API)", () -> Json.parse(charContent)); - } - - private void testImplementationDefinedSingle(String description, Runnable parseAction) { - try { - parseAction.run(); - // OK - we accepted it - } catch (JsonParseException e) { - // OK - we rejected it - } catch (StackOverflowError e) { - // OK - acceptable for deeply nested structures - LOGGER.warning("StackOverflowError on implementation-defined: " + description); - } catch (Exception e) { - // NOT OK - unexpected exception type - fail("Unexpected exception for %s: %s", description, e); - } - } -} diff --git a/json-compatibility-suite/src/test/resources/json-test-suite-data.zip b/json-compatibility-suite/src/test/resources/json-test-suite-data.zip new file mode 100644 index 0000000..dc02024 Binary files /dev/null and b/json-compatibility-suite/src/test/resources/json-test-suite-data.zip differ diff --git a/json-java21-schema/pom.xml b/json-java21-schema/pom.xml index 043a720..c861681 100644 --- a/json-java21-schema/pom.xml +++ b/json-java21-schema/pom.xml @@ -96,36 +96,6 @@ - - - - org.apache.maven.plugins - maven-antrun-plugin - 3.1.0 - - - fetch-json-schema-suite - pre-integration-test - - run - - - - - - - - - - - - - diff --git a/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java b/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java index 461f0cc..2f5511a 100644 --- a/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java +++ b/json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java @@ -663,9 +663,9 @@ static boolean scheduleRemoteIfUnseen(Deque workStack, // Detect remote cycles by walking parent chain if (formsRemoteCycle(parentMap, currentDocUri, targetDocUri)) { - String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + currentDocUri + ", target=" + targetDocUri; + String cycleMessage = "ERROR: CYCLE: remote $ref cycle detected current=" + currentDocUri + ", target=" + targetDocUri; LOG.severe(() -> cycleMessage); - throw new IllegalArgumentException(cycleMessage); + throw new IllegalStateException(cycleMessage); } // Check if already built or already in work stack @@ -1995,9 +1995,9 @@ private static JsonSchema compileInternalWithContext(Session session, JsonValue LOG.fine(() -> "Remote ref scheduling from docUri=" + docUri + " to target=" + targetDocUri); LOG.finest(() -> "Remote ref parentMap before cycle check: " + session.parentMap); if (formsRemoteCycle(session.parentMap, docUri, targetDocUri)) { - String cycleMessage = "ERROR: CYCLE: remote $ref cycle current=" + docUri + ", target=" + targetDocUri; + String cycleMessage = "ERROR: CYCLE: remote $ref cycle detected current=" + docUri + ", target=" + targetDocUri; LOG.severe(() -> cycleMessage); - throw new IllegalArgumentException(cycleMessage); + throw new IllegalStateException(cycleMessage); } boolean alreadySeen = seenUris.contains(targetDocUri); LOG.finest(() -> "Remote ref alreadySeen=" + alreadySeen + " for target=" + targetDocUri); diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java index 497dc82..9cfb13c 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java @@ -9,8 +9,13 @@ import org.junit.jupiter.api.Assumptions; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.LongAdder; import java.util.stream.Stream; @@ -22,10 +27,11 @@ /// By default, this is lenient and will SKIP mismatches and unsupported schemas /// to provide a compatibility signal without breaking the build. Enable strict /// mode with -Djson.schema.strict=true to make mismatches fail the build. +/// Test data location: see src/test/resources/JSONSchemaTestSuite-20250921/DOWNLOAD_COMMANDS.md public class JsonSchemaCheckIT { - private static final File SUITE_ROOT = - new File("target/json-schema-test-suite/tests/draft2020-12"); + private static final Path ZIP_FILE = Paths.get("src/test/resources/json-schema-test-suite-data.zip"); + private static final Path TARGET_SUITE_DIR = Paths.get("target/test-data/draft2020-12"); private static final ObjectMapper MAPPER = new ObjectMapper(); private static final boolean STRICT = Boolean.getBoolean("json.schema.strict"); private static final String METRICS_FMT = System.getProperty("json.schema.metrics", "").trim(); @@ -34,15 +40,63 @@ public class JsonSchemaCheckIT { @SuppressWarnings("resource") @TestFactory Stream runOfficialSuite() throws Exception { - return Files.walk(SUITE_ROOT.toPath()) + extractTestData(); + return Files.walk(TARGET_SUITE_DIR) .filter(p -> p.toString().endsWith(".json")) .flatMap(this::testsFromFile); } - private Stream testsFromFile(Path file) { + static void extractTestData() throws IOException { + if (!Files.exists(ZIP_FILE)) { + throw new RuntimeException("Test data ZIP file not found: " + ZIP_FILE.toAbsolutePath()); + } + + // Create target directory + Files.createDirectories(TARGET_SUITE_DIR.getParent()); + + // Extract ZIP file + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(ZIP_FILE.toFile()))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if (!entry.isDirectory() && (entry.getName().startsWith("draft2020-12/") || entry.getName().startsWith("remotes/"))) { + Path outputPath = TARGET_SUITE_DIR.resolve(entry.getName()); + Files.createDirectories(outputPath.getParent()); + Files.copy(zis, outputPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + zis.closeEntry(); + } + } + + // Verify the target directory exists after extraction + if (!Files.exists(TARGET_SUITE_DIR)) { + throw new RuntimeException("Extraction completed but target directory not found: " + TARGET_SUITE_DIR.toAbsolutePath()); + } + } + + Stream testsFromFile(Path file) { try { final var root = MAPPER.readTree(file.toFile()); + /// The JSON Schema Test Suite contains two types of files: + /// 1. Test suite files: Arrays containing test groups with description, schema, and tests fields + /// 2. Remote reference files: Plain JSON schema files used as remote references by test cases + /// + /// We only process test suite files. Remote reference files (like remotes/baseUriChangeFolder/folderInteger.json) + /// are just schema documents that get loaded via $ref during test execution, not test cases themselves. + + /// Validate that this is a test suite file (array of objects with description, schema, tests) + if (!root.isArray() || root.isEmpty()) { + // Not a test suite file, skip it + return Stream.empty(); + } + + /// Validate first group has required fields + final var firstGroup = root.get(0); + if (!firstGroup.has("description") || !firstGroup.has("schema") || !firstGroup.has("tests")) { + // Not a test suite file, skip it + return Stream.empty(); + } + /// Count groups and tests discovered final var groupCount = root.size(); METRICS.groupsDiscovered.add(groupCount); @@ -140,12 +194,12 @@ private Stream testsFromFile(Path file) { } } - private static StrictMetrics.FileCounters perFile(Path file) { + static StrictMetrics.FileCounters perFile(Path file) { return METRICS.perFile.computeIfAbsent(file.getFileName().toString(), k -> new StrictMetrics.FileCounters()); } /// Helper to check if we're running in strict mode - private static boolean isStrict() { + static boolean isStrict() { return STRICT; } @@ -195,7 +249,7 @@ static void printAndPersistMetrics() throws Exception { } } - private static String buildJsonSummary(boolean strict, String timestamp) { + static String buildJsonSummary(boolean strict, String timestamp) { var totals = new StringBuilder(); totals.append("{\n"); totals.append(" \"mode\": \"").append(strict ? "STRICT" : "LENIENT").append("\",\n"); @@ -238,7 +292,7 @@ private static String buildJsonSummary(boolean strict, String timestamp) { return totals.toString(); } - private static String buildCsvSummary(boolean strict, String timestamp) { + static String buildCsvSummary(boolean strict, String timestamp) { var csv = new StringBuilder(); csv.append("mode,timestamp,groupsDiscovered,testsDiscovered,validationsRun,passed,failed,skippedUnsupported,skipTestException,skippedMismatch\n"); csv.append(strict ? "STRICT" : "LENIENT").append(","); diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java index 136d58f..5955145 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteRefTest.java @@ -303,7 +303,7 @@ void detects_cross_document_cycle() { """), JsonSchema.Options.DEFAULT, options - )).isInstanceOf(IllegalArgumentException.class) + )).isInstanceOf(IllegalStateException.class) .hasMessageContaining("ERROR: CYCLE: remote $ref cycle"); assertThat(logs.lines().stream().anyMatch(line -> line.startsWith("ERROR: CYCLE:"))).isTrue(); } diff --git a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java index 3c34228..8325551 100644 --- a/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java +++ b/json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaRemoteServerRefTest.java @@ -25,16 +25,17 @@ void resolves_pointer_inside_remote_doc_via_http() { } @Test - void remote_cycle_handles_gracefully() { + void remote_cycle_detected_and_throws() { var policy = JsonSchema.FetchPolicy.defaults().withAllowedSchemes(Set.of("http","https")); var options = JsonSchema.CompileOptions.remoteDefaults(new VirtualThreadHttpFetcher()).withFetchPolicy(policy); - // Compilation should succeed despite the cycle - var compiled = JsonSchema.compile(Json.parse("{\"$ref\":\"" + SERVER.url("/cycle1.json") + "#\"}"), JsonSchema.Options.DEFAULT, options); - - // Validation should succeed by gracefully handling the cycle - var result = compiled.validate(Json.parse("\"test\"")); - assertThat(result.valid()).isTrue(); + // Cycles should be detected and throw an exception regardless of scheme + assertThatThrownBy(() -> JsonSchema.compile( + Json.parse("{\"$ref\":\"" + SERVER.url("/cycle1.json") + "#\"}"), + JsonSchema.Options.DEFAULT, + options + )).isInstanceOf(IllegalStateException.class) + .hasMessageContaining("ERROR: CYCLE: remote $ref cycle"); } } diff --git a/json-java21-schema/src/test/resources/json-schema-test-suite-data.zip b/json-java21-schema/src/test/resources/json-schema-test-suite-data.zip new file mode 100644 index 0000000..938fed0 Binary files /dev/null and b/json-java21-schema/src/test/resources/json-schema-test-suite-data.zip differ