Repository: networknt/json-schema-validator (java)

Issue:
improve outputUnits with all assertion fields 
Thanks for your great lib.
We needed json schema validation for our scala project, and it was really easy to wrap around your work !

I have one suggestion regardless: 
You have documented a lot of interesting fields available for the "assertion" object here
https://github.com/networknt/json-schema-validator?tab=readme-ov-file#results-and-output-formats
that are in fact only available in the `ValidationMessage` class and not in `OutputUnit` for LIST or HIERARCHY validation.
It would be great to have them, for example to be able to built our own error message from the given errors without the need to rely on the lib specific formulation.

In this view, I think `OutputUnit.getErrors` and `OutputUnit.getAnnotations` could return something with stronger type than just `Map<String, Object>` to give structure, in a way close to what already exists in `ValidationMessage`

I hope it helps improve this great lib ! 

Pull request description:
Allow customization of assertion for outputunit
Closes #1030

This adds a constructor argument for `OutputFormat.Hierarchical` and `OutputFormat.List` to add a function to allow mapping of the `ValidationMessage` to an `Object`. This allows the message to be customized or allows the `ValidationMessage` to be passed through as is if the identity function is used.

Patch:
diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java
index cddc963..3476555 100644
--- a/src/main/java/com/networknt/schema/OutputFormat.java
+++ b/src/main/java/com/networknt/schema/OutputFormat.java
@@ -16,11 +16,13 @@
 package com.networknt.schema;
 
 import java.util.Set;
+import java.util.function.Function;
 
 import com.networknt.schema.output.HierarchicalOutputUnitFormatter;
 import com.networknt.schema.output.ListOutputUnitFormatter;
 import com.networknt.schema.output.OutputFlag;
 import com.networknt.schema.output.OutputUnit;
+import com.networknt.schema.output.OutputUnitData;
 
 /**
  * Formats the validation results.
@@ -132,6 +134,21 @@ public interface OutputFormat<T> {
      * The List output format.
      */
     public static class List implements OutputFormat<OutputUnit> {
+        private final Function<ValidationMessage, Object> assertionMapper;
+
+        public List() {
+            this(OutputUnitData::formatAssertion);
+        }
+
+        /**
+         * Constructor.
+         * 
+         * @param assertionMapper to map the assertion
+         */
+        public List(Function<ValidationMessage, Object> assertionMapper) {
+            this.assertionMapper = assertionMapper;
+        }
+
         @Override
         public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
         }
@@ -139,7 +156,8 @@ public interface OutputFormat<T> {
         @Override
         public OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> validationMessages,
                 ExecutionContext executionContext, ValidationContext validationContext) {
-            return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext);
+            return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext,
+                    this.assertionMapper);
         }
     }
 
@@ -147,6 +165,21 @@ public interface OutputFormat<T> {
      * The Hierarchical output format.
      */
     public static class Hierarchical implements OutputFormat<OutputUnit> {
+        private final Function<ValidationMessage, Object> assertionMapper;
+
+        public Hierarchical() {
+            this(OutputUnitData::formatAssertion);
+        }
+
+        /**
+         * Constructor.
+         * 
+         * @param assertionMapper to map the assertion
+         */
+        public Hierarchical(Function<ValidationMessage, Object> assertionMapper) {
+            this.assertionMapper = assertionMapper;
+        }
+
         @Override
         public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
         }
@@ -154,7 +187,8 @@ public interface OutputFormat<T> {
         @Override
         public OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> validationMessages,
                 ExecutionContext executionContext, ValidationContext validationContext) {
-            return HierarchicalOutputUnitFormatter.format(jsonSchema, validationMessages, executionContext, validationContext);
+            return HierarchicalOutputUnitFormatter.format(jsonSchema, validationMessages, executionContext,
+                    validationContext, this.assertionMapper);
         }
     }
 }
diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java
index a557b32..e4cffae 100644
--- a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java
+++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java
@@ -22,6 +22,7 @@ import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Function;
 import java.util.Set;
 
 import com.networknt.schema.ExecutionContext;
@@ -34,18 +35,7 @@ import com.networknt.schema.ValidationMessage;
  * HierarchicalOutputUnitFormatter.
  */
 public class HierarchicalOutputUnitFormatter {
-    public static OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> validationMessages,
-            ExecutionContext executionContext, ValidationContext validationContext) {
-        
-        OutputUnit root = new OutputUnit();
-        root.setValid(validationMessages.isEmpty());
-        
-        root.setInstanceLocation(validationContext.getConfig().getPathType().getRoot());
-        root.setEvaluationPath(validationContext.getConfig().getPathType().getRoot());
-        root.setSchemaLocation(jsonSchema.getSchemaLocation().toString());
-
-        OutputUnitData data = OutputUnitData.from(validationMessages, executionContext);
-
+    public static OutputUnit format(OutputUnit root, OutputUnitData data, JsonNodePath rootPath) {
         Map<OutputUnitKey, Boolean> valid = data.getValid();
         Map<OutputUnitKey, Map<String, Object>> errors = data.getErrors();
         Map<OutputUnitKey, Map<String, Object>> annotations = data.getAnnotations();
@@ -54,8 +44,8 @@ public class HierarchicalOutputUnitFormatter {
         // Evaluation path to output unit
         Map<JsonNodePath, Map<JsonNodePath, OutputUnit>> index = new LinkedHashMap<>();
         Map<JsonNodePath, OutputUnit> r = new LinkedHashMap<>();
-        r.put(new JsonNodePath(validationContext.getConfig().getPathType()), root);
-        index.put(new JsonNodePath(validationContext.getConfig().getPathType()), r);
+        r.put(rootPath, root);
+        index.put(rootPath, r);
         
         // Get all the evaluation paths with data
         // This is a map of evaluation path to instance location
@@ -116,6 +106,21 @@ public class HierarchicalOutputUnitFormatter {
         }
         return root;
     }
+
+    public static OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> validationMessages,
+            ExecutionContext executionContext, ValidationContext validationContext,
+            Function<ValidationMessage, Object> assertionMapper) {
+        OutputUnit root = new OutputUnit();
+        root.setValid(validationMessages.isEmpty());
+        
+        root.setInstanceLocation(validationContext.getConfig().getPathType().getRoot());
+        root.setEvaluationPath(validationContext.getConfig().getPathType().getRoot());
+        root.setSchemaLocation(jsonSchema.getSchemaLocation().toString());
+
+        OutputUnitData data = OutputUnitData.from(validationMessages, executionContext, assertionMapper);
+        
+        return format(root, data, new JsonNodePath(validationContext.getConfig().getPathType()));
+    }
     
     /**
      * Builds in the index of evaluation path to output units to be populated later
diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java
index 290a1a5..529eb34 100644
--- a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java
+++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java
@@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.function.Function;
 import java.util.Set;
 
 import com.networknt.schema.ExecutionContext;
@@ -30,13 +31,7 @@ import com.networknt.schema.ValidationMessage;
  * ListOutputUnitFormatter.
  */
 public class ListOutputUnitFormatter {
-    public static OutputUnit format(Set<ValidationMessage> validationMessages, ExecutionContext executionContext,
-            ValidationContext validationContext) {
-        OutputUnit root = new OutputUnit();
-        root.setValid(validationMessages.isEmpty());
-
-        OutputUnitData data = OutputUnitData.from(validationMessages, executionContext);
-
+    public static OutputUnit format(OutputUnit root, OutputUnitData data) {
         Map<OutputUnitKey, Boolean> valid = data.getValid();
         Map<OutputUnitKey, Map<String, Object>> errors = data.getErrors();
         Map<OutputUnitKey, Map<String, Object>> annotations = data.getAnnotations();
@@ -95,4 +90,11 @@ public class ListOutputUnitFormatter {
 
         return root;
     }
+
+    public static OutputUnit format(Set<ValidationMessage> validationMessages, ExecutionContext executionContext,
+            ValidationContext validationContext, Function<ValidationMessage, Object> assertionMapper) {
+        OutputUnit root = new OutputUnit();
+        root.setValid(validationMessages.isEmpty());
+        return format(root, OutputUnitData.from(validationMessages, executionContext, assertionMapper));
+    }
 }
diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java
index e52d3a1..7489c8f 100644
--- a/src/main/java/com/networknt/schema/output/OutputUnitData.java
+++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java
@@ -20,6 +20,7 @@ import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 import com.networknt.schema.ExecutionContext;
 import com.networknt.schema.SchemaLocation;
@@ -51,6 +52,10 @@ public class OutputUnitData {
         return droppedAnnotations;
     }
 
+    public static String formatAssertion(ValidationMessage validationMessage) {
+        return formatMessage(validationMessage.getMessage());
+    }
+
     public static String formatMessage(String message) {
         int index = message.indexOf(":");
         if (index != -1) {
@@ -68,7 +73,8 @@ public class OutputUnitData {
     }
 
     @SuppressWarnings("unchecked")
-    public static OutputUnitData from(Set<ValidationMessage> validationMessages, ExecutionContext executionContext) {
+    public static OutputUnitData from(Set<ValidationMessage> validationMessages, ExecutionContext executionContext,
+            Function<ValidationMessage, Object> assertionMapper) {
         OutputUnitData data = new OutputUnitData();
 
         Map<OutputUnitKey, Boolean> valid = data.valid;
@@ -85,15 +91,15 @@ public class OutputUnitData {
             Map<String, Object> errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>());
             Object value = errorMap.get(assertion.getType());
             if (value == null) {
-                errorMap.put(assertion.getType(), formatMessage(assertion.getMessage()));
+                errorMap.put(assertion.getType(), assertionMapper.apply(assertion));
             } else {
                 // Existing error, make it into a list
                 if (value instanceof List) {
-                    ((List<String>) value).add(formatMessage(assertion.getMessage()));
+                    ((List<Object>) value).add(assertionMapper.apply(assertion));
                 } else {
-                    List<String> values = new ArrayList<>();
+                    List<Object> values = new ArrayList<>();
                     values.add(value.toString());
-                    values.add(formatMessage(assertion.getMessage()));
+                    values.add(assertionMapper.apply(assertion));
                     errorMap.put(assertion.getType(), values);
                 }
             }


Test patch:
diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java
index f613a13..035aa70 100644
--- a/src/test/java/com/networknt/schema/OutputUnitTest.java
+++ b/src/test/java/com/networknt/schema/OutputUnitTest.java
@@ -17,6 +17,7 @@ package com.networknt.schema;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -339,4 +340,34 @@ public class OutputUnitTest {
         assertEquals(expected, output);
     }
 
+    @Test
+    void listAssertionMapper() {
+        String formatSchema = "{\r\n"
+                + "  \"type\": \"string\"\r\n"
+                + "}";
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+        config.setPathType(PathType.JSON_POINTER);
+        JsonSchema schema = factory.getSchema(formatSchema, config);
+        OutputUnit outputUnit = schema.validate("1234", InputFormat.JSON, new OutputFormat.List(a -> a));
+        assertFalse(outputUnit.isValid());
+        OutputUnit details = outputUnit.getDetails().get(0);
+        Object assertion = details.getErrors().get("type");
+        assertInstanceOf(ValidationMessage.class, assertion);
+    }
+
+    @Test
+    void hierarchicalAssertionMapper() {
+        String formatSchema = "{\r\n"
+                + "  \"type\": \"string\"\r\n"
+                + "}";
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+        config.setPathType(PathType.JSON_POINTER);
+        JsonSchema schema = factory.getSchema(formatSchema, config);
+        OutputUnit outputUnit = schema.validate("1234", InputFormat.JSON, new OutputFormat.Hierarchical(a -> a));
+        assertFalse(outputUnit.isValid());
+        Object assertion = outputUnit.getErrors().get("type");
+        assertInstanceOf(ValidationMessage.class, assertion);
+    }
 }
diff --git a/src/test/java/com/networknt/schema/UriMappingTest.java b/src/test/java/com/networknt/schema/UriMappingTest.java
index 9d6fe61..3616704 100644
--- a/src/test/java/com/networknt/schema/UriMappingTest.java
+++ b/src/test/java/com/networknt/schema/UriMappingTest.java
@@ -76,7 +76,7 @@ public class UriMappingTest {
             fail("Expected exception not thrown");
         } catch (JsonSchemaException ex) {
             Throwable cause = ex.getCause();
-            if (!(cause instanceof FileNotFoundException || cause instanceof UnknownHostException)) {
+            if (!(cause instanceof IOException )) {
                 fail("Unexpected cause for JsonSchemaException", ex);
             }
             // passing, so do nothing
@@ -132,7 +132,7 @@ public class UriMappingTest {
             fail("Expected exception not thrown");
         } catch (JsonSchemaException ex) {
             Throwable cause = ex.getCause();
-            if (!(cause instanceof FileNotFoundException || cause instanceof UnknownHostException)) {
+            if (!(cause instanceof IOException)) {
                 fail("Unexpected cause for JsonSchemaException");
             }
             // passing, so do nothing