11package io .github .simbo1905 .json .schema ;
22
3- import com .fasterxml .jackson .databind .JsonNode ;
4- import com .fasterxml .jackson .databind .ObjectMapper ;
5- import jdk .sandbox .java .util .json .Json ;
63import org .junit .jupiter .api .DynamicTest ;
74import org .junit .jupiter .api .TestFactory ;
8- import org .junit .jupiter .api .AfterAll ;
9- import org .junit .jupiter .api .Assumptions ;
105
11- import java .io .FileInputStream ;
12- import java .io .IOException ;
13- import java .nio .file .Files ;
146import java .nio .file .Path ;
157import java .nio .file .Paths ;
16- import java .util .zip .ZipEntry ;
17- import java .util .zip .ZipInputStream ;
8+ import java .util .Set ;
189import java .util .stream .Stream ;
19- import java .util .stream .StreamSupport ;
20-
21- import static org .junit .jupiter .api .Assertions .assertEquals ;
2210
2311/// Runs the official JSON-Schema-Test-Suite (Draft 2020-12) as JUnit dynamic tests.
2412/// By default, this is lenient and will SKIP mismatches and unsupported schemas
2513/// to provide a compatibility signal without breaking the build. Enable strict
2614/// mode with -Djson.schema.strict=true to make mismatches fail the build.
27- public class JsonSchemaCheck202012IT {
28-
29- private static final Path ZIP_FILE = Paths .get ("src/test/resources/json-schema-test-suite-data.zip" );
30- private static final Path TARGET_SUITE_DIR = Paths .get ("target/test-data/draft2020-12" );
31- private static final ObjectMapper MAPPER = new ObjectMapper ();
32- private static final boolean STRICT = Boolean .getBoolean ("json.schema.strict" );
33- private static final String METRICS_FMT = System .getProperty ("json.schema.metrics" , "" ).trim ();
34- private static final StrictMetrics METRICS = new StrictMetrics ();
35-
36- @ SuppressWarnings ("resource" )
37- @ TestFactory
38- Stream <DynamicTest > runOfficialSuite () throws Exception {
39- extractTestData ();
40- return Files .walk (TARGET_SUITE_DIR )
41- .filter (p -> p .toString ().endsWith (".json" ))
42- .flatMap (this ::testsFromFile );
43- }
44-
45- static void extractTestData () throws IOException {
46- if (!Files .exists (ZIP_FILE )) {
47- throw new RuntimeException ("Test data ZIP file not found: " + ZIP_FILE .toAbsolutePath ());
48- }
49-
50- // Create target directory
51- Files .createDirectories (TARGET_SUITE_DIR .getParent ());
52-
53- // Extract ZIP file
54- try (ZipInputStream zis = new ZipInputStream (new FileInputStream (ZIP_FILE .toFile ()))) {
55- ZipEntry entry ;
56- while ((entry = zis .getNextEntry ()) != null ) {
57- if (!entry .isDirectory () && (entry .getName ().startsWith ("draft2020-12/" ) || entry .getName ().startsWith ("remotes/" ))) {
58- Path outputPath = TARGET_SUITE_DIR .resolve (entry .getName ());
59- Files .createDirectories (outputPath .getParent ());
60- Files .copy (zis , outputPath , java .nio .file .StandardCopyOption .REPLACE_EXISTING );
61- }
62- zis .closeEntry ();
63- }
64- }
65-
66- // Verify the target directory exists after extraction
67- if (!Files .exists (TARGET_SUITE_DIR )) {
68- throw new RuntimeException ("Extraction completed but target directory not found: " + TARGET_SUITE_DIR .toAbsolutePath ());
69- }
70- }
71-
72- Stream <DynamicTest > testsFromFile (Path file ) {
73- try {
74- final var root = MAPPER .readTree (file .toFile ());
75-
76- /// The JSON Schema Test Suite contains two types of files:
77- /// 1. Test suite files: Arrays containing test groups with description, schema, and tests fields
78- /// 2. Remote reference files: Plain JSON schema files used as remote references by test cases
79- ///
80- /// We only process test suite files. Remote reference files (like remotes/baseUriChangeFolder/folderInteger.json)
81- /// are just schema documents that get loaded via $ref during test execution, not test cases themselves.
82-
83- /// Validate that this is a test suite file (array of objects with description, schema, tests)
84- if (!root .isArray () || root .isEmpty ()) {
85- // Not a test suite file, skip it
86- return Stream .empty ();
87- }
88-
89- /// Validate first group has required fields
90- final var firstGroup = root .get (0 );
91- if (!firstGroup .has ("description" ) || !firstGroup .has ("schema" ) || !firstGroup .has ("tests" )) {
92- // Not a test suite file, skip it
93- return Stream .empty ();
94- }
95-
96- /// Count groups and tests discovered
97- final var groupCount = root .size ();
98- METRICS .groupsDiscovered .add (groupCount );
99- perFile (file ).groups .add (groupCount );
100-
101- var testCount = 0 ;
102- for (final var group : root ) {
103- testCount += group .get ("tests" ).size ();
104- }
105- METRICS .testsDiscovered .add (testCount );
106- perFile (file ).tests .add (testCount );
107-
108- return dynamicTestStream (file , root );
109- } catch (Exception ex ) {
110- throw new RuntimeException ("Failed to process " + file , ex );
111- }
112- }
113-
114- static Stream <DynamicTest > dynamicTestStream (Path file , JsonNode root ) {
115- return StreamSupport .stream (root .spliterator (), false )
116- .flatMap (group -> {
117- final var groupDesc = group .get ("description" ).asText ();
118- try {
119- /// Attempt to compile the schema for this group; if unsupported features
120- /// (e.g., unresolved anchors) are present, skip this group gracefully.
121- final var schema = JsonSchema .compile (
122- Json .parse (group .get ("schema" ).toString ()));
123-
124- return StreamSupport .stream (group .get ("tests" ).spliterator (), false )
125- .map (test -> DynamicTest .dynamicTest (
126- groupDesc + " – " + test .get ("description" ).asText (),
127- () -> {
128- final var expected = test .get ("valid" ).asBoolean ();
129- final boolean actual ;
130- try {
131- actual = schema .validate (
132- Json .parse (test .get ("data" ).toString ())).valid ();
133-
134- /// Count validation attempt
135- METRICS .run .increment ();
136- perFile (file ).run .increment ();
137- } catch (Exception e ) {
138- final var reason = e .getMessage () == null ? e .getClass ().getSimpleName () : e .getMessage ();
139- System .err .println ("[JsonSchemaCheck202012IT] Skipping test due to exception: "
140- + groupDesc + " — " + reason + " (" + file .getFileName () + ")" );
141-
142- /// Count exception as skipped mismatch in strict metrics
143- METRICS .skippedMismatch .increment ();
144- perFile (file ).skipMismatch .increment ();
145-
146- if (isStrict ()) throw e ;
147- Assumptions .assumeTrue (false , "Skipped: " + reason );
148- return ; /// not reached when strict
149- }
150-
151- if (isStrict ()) {
152- try {
153- assertEquals (expected , actual );
154- /// Count pass in strict mode
155- METRICS .passed .increment ();
156- perFile (file ).pass .increment ();
157- } catch (AssertionError e ) {
158- /// Count failure in strict mode
159- METRICS .failed .increment ();
160- perFile (file ).fail .increment ();
161- throw e ;
162- }
163- } else if (expected != actual ) {
164- System .err .println ("[JsonSchemaCheck202012IT] Mismatch (ignored): "
165- + groupDesc + " — expected=" + expected + ", actual=" + actual
166- + " (" + file .getFileName () + ")" );
15+ public class JsonSchemaCheck202012IT extends JsonSchemaCheckBaseIT {
16716
168- /// Count lenient mismatch skip
169- METRICS .skippedMismatch .increment ();
170- perFile (file ).skipMismatch .increment ();
17+ private static final Path ZIP_FILE = Paths .get ("src/test/resources/json-schema-test-suite-data.zip" );
18+ private static final Path TARGET_SUITE_DIR = Paths .get ("target/test-data/draft2020-12" );
17119
172- Assumptions .assumeTrue (false , "Mismatch ignored" );
173- } else {
174- /// Count pass in lenient mode
175- METRICS .passed .increment ();
176- perFile (file ).pass .increment ();
177- }
178- }));
179- } catch (Exception ex ) {
180- /// Unsupported schema for this group; emit a single skipped test for visibility
181- final var reason = ex .getMessage () == null ? ex .getClass ().getSimpleName () : ex .getMessage ();
182- System .err .println ("[JsonSchemaCheck202012IT] Skipping group due to unsupported schema: "
183- + groupDesc + " — " + reason + " (" + file .getFileName () + ")" );
184-
185- /// Count unsupported group skip
186- METRICS .skippedUnsupported .increment ();
187- perFile (file ).skipUnsupported .increment ();
188-
189- return Stream .of (DynamicTest .dynamicTest (
190- groupDesc + " – SKIPPED: " + reason ,
191- () -> { if (isStrict ()) throw ex ; Assumptions .assumeTrue (false , "Unsupported schema: " + reason ); }
192- ));
193- }
194- });
20+ @ Override
21+ protected Path getZipFile () {
22+ return ZIP_FILE ;
19523 }
19624
197- static StrictMetrics .FileCounters perFile (Path file ) {
198- return METRICS .perFile .computeIfAbsent (file .getFileName ().toString (), k -> new StrictMetrics .FileCounters ());
199- }
200-
201- /// Helper to check if we're running in strict mode
202- static boolean isStrict () {
203- return STRICT ;
204- }
205-
206- @ AfterAll
207- static void printAndPersistMetrics () throws Exception {
208- final var strict = isStrict ();
209- final var total = METRICS .testsDiscovered .sum ();
210- final var run = METRICS .run .sum ();
211- final var passed = METRICS .passed .sum ();
212- final var failed = METRICS .failed .sum ();
213- final var skippedUnsupported = METRICS .skippedUnsupported .sum ();
214- final var skippedMismatch = METRICS .skippedMismatch .sum ();
215-
216- /// Print canonical summary line
217- System .out .printf (
218- "JSON-SCHEMA-COMPAT: total=%d run=%d passed=%d failed=%d skipped-unsupported=%d skipped-mismatch=%d strict=%b%n" ,
219- total , run , passed , failed , skippedUnsupported , skippedMismatch , strict
220- );
221-
222- /// For accounting purposes, we accept that the current implementation
223- /// creates some accounting complexity when groups are skipped.
224- /// The key metrics are still valid and useful for tracking progress.
225- if (strict ) {
226- assertEquals (run , passed + failed , "strict run accounting mismatch" );
227- }
228-
229- /// Legacy metrics for backward compatibility
230- System .out .printf (
231- "JSON-SCHEMA SUITE (%s): groups=%d testsScanned=%d run=%d passed=%d failed=%d skipped={unsupported=%d, exception=%d, lenientMismatch=%d}%n" ,
232- strict ? "STRICT" : "LENIENT" ,
233- METRICS .groupsDiscovered .sum (),
234- METRICS .testsDiscovered .sum (),
235- run , passed , failed , skippedUnsupported , METRICS .skipTestException .sum (), skippedMismatch
236- );
237-
238- if (!METRICS_FMT .isEmpty ()) {
239- var outDir = java .nio .file .Path .of ("target" );
240- java .nio .file .Files .createDirectories (outDir );
241- var ts = java .time .OffsetDateTime .now ().toString ();
242- if ("json" .equalsIgnoreCase (METRICS_FMT )) {
243- var json = buildJsonSummary (strict , ts );
244- java .nio .file .Files .writeString (outDir .resolve ("json-schema-compat.json" ), json );
245- } else if ("csv" .equalsIgnoreCase (METRICS_FMT )) {
246- var csv = buildCsvSummary (strict , ts );
247- java .nio .file .Files .writeString (outDir .resolve ("json-schema-compat.csv" ), csv );
248- }
249- }
250- }
25+ @ Override
26+ protected Path getTargetSuiteDir () {
27+ return TARGET_SUITE_DIR ;
28+ }
25129
252- static String buildJsonSummary (boolean strict , String timestamp ) {
253- var totals = new StringBuilder ();
254- totals .append ("{\n " );
255- totals .append (" \" mode\" : \" " ).append (strict ? "STRICT" : "LENIENT" ).append ("\" ,\n " );
256- totals .append (" \" timestamp\" : \" " ).append (timestamp ).append ("\" ,\n " );
257- totals .append (" \" totals\" : {\n " );
258- totals .append (" \" groupsDiscovered\" : " ).append (METRICS .groupsDiscovered .sum ()).append (",\n " );
259- totals .append (" \" testsDiscovered\" : " ).append (METRICS .testsDiscovered .sum ()).append (",\n " );
260- totals .append (" \" validationsRun\" : " ).append (METRICS .run .sum ()).append (",\n " );
261- totals .append (" \" passed\" : " ).append (METRICS .passed .sum ()).append (",\n " );
262- totals .append (" \" failed\" : " ).append (METRICS .failed .sum ()).append (",\n " );
263- totals .append (" \" skipped\" : {\n " );
264- totals .append (" \" unsupportedSchemaGroup\" : " ).append (METRICS .skippedUnsupported .sum ()).append (",\n " );
265- totals .append (" \" testException\" : " ).append (METRICS .skipTestException .sum ()).append (",\n " );
266- totals .append (" \" lenientMismatch\" : " ).append (METRICS .skippedMismatch .sum ()).append ("\n " );
267- totals .append (" }\n " );
268- totals .append (" },\n " );
269- totals .append (" \" perFile\" : [\n " );
270-
271- var files = new java .util .ArrayList <String >(METRICS .perFile .keySet ());
272- java .util .Collections .sort (files );
273- var first = true ;
274- for (String file : files ) {
275- var counters = METRICS .perFile .get (file );
276- if (!first ) totals .append (",\n " );
277- first = false ;
278- totals .append (" {\n " );
279- totals .append (" \" file\" : \" " ).append (file ).append ("\" ,\n " );
280- totals .append (" \" groups\" : " ).append (counters .groups .sum ()).append (",\n " );
281- totals .append (" \" tests\" : " ).append (counters .tests .sum ()).append (",\n " );
282- totals .append (" \" run\" : " ).append (counters .run .sum ()).append (",\n " );
283- totals .append (" \" pass\" : " ).append (counters .pass .sum ()).append (",\n " );
284- totals .append (" \" fail\" : " ).append (counters .fail .sum ()).append (",\n " );
285- totals .append (" \" skipUnsupported\" : " ).append (counters .skipUnsupported .sum ()).append (",\n " );
286- totals .append (" \" skipException\" : " ).append (counters .skipException .sum ()).append (",\n " );
287- totals .append (" \" skipMismatch\" : " ).append (counters .skipMismatch .sum ()).append ("\n " );
288- totals .append (" }" );
289- }
290- totals .append ("\n ]\n " );
291- totals .append ("}\n " );
292- return totals .toString ();
293- }
30+ @ Override
31+ protected String getSchemaPrefix () {
32+ return "draft2020-12/" ;
33+ }
29434
295- static String buildCsvSummary (boolean strict , String timestamp ) {
296- var csv = new StringBuilder ();
297- csv .append ("mode,timestamp,groupsDiscovered,testsDiscovered,validationsRun,passed,failed,skippedUnsupported,skipTestException,skippedMismatch\n " );
298- csv .append (strict ? "STRICT" : "LENIENT" ).append ("," );
299- csv .append (timestamp ).append ("," );
300- csv .append (METRICS .groupsDiscovered .sum ()).append ("," );
301- csv .append (METRICS .testsDiscovered .sum ()).append ("," );
302- csv .append (METRICS .run .sum ()).append ("," );
303- csv .append (METRICS .passed .sum ()).append ("," );
304- csv .append (METRICS .failed .sum ()).append ("," );
305- csv .append (METRICS .skippedUnsupported .sum ()).append ("," );
306- csv .append (METRICS .skipTestException .sum ()).append ("," );
307- csv .append (METRICS .skippedMismatch .sum ()).append ("\n " );
308-
309- csv .append ("\n perFile breakdown:\n " );
310- csv .append ("file,groups,tests,run,pass,fail,skipUnsupported,skipException,skipMismatch\n " );
311-
312- var files = new java .util .ArrayList <String >(METRICS .perFile .keySet ());
313- java .util .Collections .sort (files );
314- for (String file : files ) {
315- var counters = METRICS .perFile .get (file );
316- csv .append (file ).append ("," );
317- csv .append (counters .groups .sum ()).append ("," );
318- csv .append (counters .tests .sum ()).append ("," );
319- csv .append (counters .run .sum ()).append ("," );
320- csv .append (counters .pass .sum ()).append ("," );
321- csv .append (counters .fail .sum ()).append ("," );
322- csv .append (counters .skipUnsupported .sum ()).append ("," );
323- csv .append (counters .skipException .sum ()).append ("," );
324- csv .append (counters .skipMismatch .sum ()).append ("\n " );
325- }
326- return csv .toString ();
327- }
328- }
35+ @ Override
36+ protected Set <String > getSkippedTests () {
37+ return Set .of (
38+ // Reference resolution issues - Unresolved $ref problems
39+ "ref.json#relative pointer ref to array#match array" ,
40+ "ref.json#relative pointer ref to array#mismatch array" ,
41+ "refOfUnknownKeyword.json#reference of a root arbitrary keyword #match" ,
42+ "refOfUnknownKeyword.json#reference of a root arbitrary keyword #mismatch" ,
43+ "refOfUnknownKeyword.json#reference of an arbitrary keyword of a sub-schema#match" ,
44+ "refOfUnknownKeyword.json#reference of an arbitrary keyword of a sub-schema#mismatch" ,
45+
46+ // JSON parsing issues with duplicate member names
47+ "required.json#required with escaped characters#object with all properties present is valid" ,
48+ "required.json#required with escaped characters#object with some properties missing is invalid"
49+ );
50+ }
32951
52+ @ TestFactory
53+ @ Override
54+ public Stream <DynamicTest > runOfficialSuite () throws Exception {
55+ return super .runOfficialSuite ();
56+ }
57+ }
0 commit comments