Repository: networknt/json-schema-validator
Pull request #492 (2022-01-05 21:13:47)

Sort ValidationMessage by its type


Patch:
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index 2a61073..cc97459 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -186,9 +186,10 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
         else if (numberOfValidSchema < 1) {
             if (!childErrors.isEmpty()) {
                 if (childErrors.size() > 1) {
-                    Set<ValidationMessage> notAdditionalPropertiesOnly = childErrors.stream()
+                    Set<ValidationMessage> notAdditionalPropertiesOnly = new LinkedHashSet<>(childErrors.stream()
                             .filter((ValidationMessage validationMessage) -> !ValidatorTypeCode.ADDITIONAL_PROPERTIES.getValue().equals(validationMessage.getType()))
-                            .collect(Collectors.toSet());
+                            .sorted((vm1, vm2) -> compareValidationMessages(vm1, vm2))
+                            .collect(Collectors.toList()));
                     if (notAdditionalPropertiesOnly.size() > 0) {
                         childErrors = notAdditionalPropertiesOnly;
                     }
@@ -210,6 +211,44 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
         return Collections.unmodifiableSet(errors);
     }
 
+    /**
+     * Sort <code>ValidationMessage</code> by its type
+     * @return
+     */
+    private static int compareValidationMessages(ValidationMessage vm1, ValidationMessage vm2) {
+        // ValidationMessage's type has smaller index in the list below has high priority
+        final List<String> typeCodes = Arrays.asList(
+                ValidatorTypeCode.TYPE.getValue(),
+                ValidatorTypeCode.DATETIME.getValue(),
+                ValidatorTypeCode.UUID.getValue(),
+                ValidatorTypeCode.ID.getValue(),
+                ValidatorTypeCode.EXCLUSIVE_MAXIMUM.getValue(),
+                ValidatorTypeCode.EXCLUSIVE_MINIMUM.getValue(),
+                ValidatorTypeCode.TRUE.getValue(),
+                ValidatorTypeCode.FALSE.getValue(),
+                ValidatorTypeCode.CONST.getValue(),
+                ValidatorTypeCode.CONTAINS.getValue(),
+                ValidatorTypeCode.PROPERTYNAMES.getValue()
+        );
+
+        final int index1 = typeCodes.indexOf(vm1.getType());
+        final int index2 = typeCodes.indexOf(vm2.getType());
+
+        if (index1 >= 0) {
+            if (index2 >= 0) {
+                return Integer.compare(index1, index2);
+            } else {
+                return -1;
+            }
+        } else {
+            if (index2 >= 0) {
+                return 1;
+            } else {
+                return vm1.getCode().compareTo(vm2.getCode());
+            }
+        }
+    }
+
     private void resetValidatorState() {
         ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
         state.setComplexValidator(false);


Test patch:
diff --git a/src/test/java/com/networknt/schema/Issue491Test.java b/src/test/java/com/networknt/schema/Issue491Test.java
new file mode 100644
index 0000000..84fea63
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue491Test.java
@@ -0,0 +1,153 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.InputStream;
+import java.util.Set;
+import java.util.stream.Stream;
+
+class Issue491Test {
+
+    private static JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+    private static String schemaPath1 = "/schema/issue491-v7.json";
+    private static String schemaPath2 = "/schema/issue491_2-v7.json";
+    private static String schemaPath3 = "/schema/issue491_3-v7.json";
+
+    private JsonNode getJsonNodeFromJsonData(String jsonFilePath) throws Exception {
+        InputStream content = getClass().getResourceAsStream(jsonFilePath);
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readTree(content);
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 1")
+    void testValidJson1() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-1.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 2")
+    void testValidJson2() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 1")
+    void testValidJson3() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath2);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-3.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 2")
+    void testValidJson4() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath2);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 1")
+    void testValidJson5() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath3);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-4.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid oneOf option 2")
+    void testValidJson6() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath3);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-valid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test invalid oneOf option 1 - wrong type")
+    void testInvalidJson1() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-invalid-1.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+        Assertions.assertEquals("$.search.searchAge.age: string found, integer expected", errors.iterator().next().getMessage());
+    }
+
+    @Test
+    @DisplayName("Test invalid oneOf option 2 - wrong type")
+    void testInvalidJson2() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-invalid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+        Assertions.assertEquals("$.search.name: integer found, string expected", errors.iterator().next().getMessage());
+    }
+
+    @Test
+    @DisplayName("Test invalid oneOf option 1 - wrong type")
+    void testInvalidJson3() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath2);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-invalid-3.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+        Assertions.assertEquals("$.search.byAge.age: string found, integer expected", errors.iterator().next().getMessage());
+    }
+
+    @Test
+    @DisplayName("Test invalid oneOf option 2 - wrong type")
+    void testInvalidJson4() throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath2);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue491-invalid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+        Assertions.assertEquals("$.search.name: integer found, string expected", errors.iterator().next().getMessage());
+    }
+
+    @ParameterizedTest
+    @MethodSource("parametersProvider")
+    @DisplayName("Test invalid oneOf option - wrong types or values")
+    void testInvalidJson5(String jsonPath, String expectedError) throws Exception {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath3);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData(jsonPath);
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+        Assertions.assertEquals(expectedError, errors.iterator().next().getMessage());
+    }
+
+    private static Stream<Arguments> parametersProvider() {
+        return Stream.of(
+                Arguments.of("/data/issue491-invalid-4.json", "$.search.age: string found, integer expected"),
+                Arguments.of("/data/issue491-invalid-2.json", "$.search.name: integer found, string expected"),
+                Arguments.of("/data/issue491-invalid-5.json", "$.search.age: must have a maximum value of 150"),
+                Arguments.of("/data/issue491-invalid-6.json", "$.search.name: may only be 20 characters long")
+        );
+    }
+}
diff --git a/src/test/resources/data/issue491-invalid-1.json b/src/test/resources/data/issue491-invalid-1.json
new file mode 100644
index 0000000..80dd105
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-1.json
@@ -0,0 +1,7 @@
+{
+  "search": {
+    "searchAge": {
+      "age": "Steve"
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-invalid-2.json b/src/test/resources/data/issue491-invalid-2.json
new file mode 100644
index 0000000..29f140c
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-2.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "name": 123
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-invalid-3.json b/src/test/resources/data/issue491-invalid-3.json
new file mode 100644
index 0000000..37878f8
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-3.json
@@ -0,0 +1,7 @@
+{
+  "search": {
+    "byAge": {
+      "age": "Steve"
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-invalid-4.json b/src/test/resources/data/issue491-invalid-4.json
new file mode 100644
index 0000000..79c8d4f
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-4.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "age": "Steve"
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-invalid-5.json b/src/test/resources/data/issue491-invalid-5.json
new file mode 100644
index 0000000..9528725
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-5.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "age": 200
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-invalid-6.json b/src/test/resources/data/issue491-invalid-6.json
new file mode 100644
index 0000000..3fad4f3
--- /dev/null
+++ b/src/test/resources/data/issue491-invalid-6.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "name": "TooLoooooooooooooooooooooooooooooooooongName"
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-valid-1.json b/src/test/resources/data/issue491-valid-1.json
new file mode 100644
index 0000000..edc86d3
--- /dev/null
+++ b/src/test/resources/data/issue491-valid-1.json
@@ -0,0 +1,7 @@
+{
+  "search": {
+    "searchAge": {
+      "age": 50
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-valid-2.json b/src/test/resources/data/issue491-valid-2.json
new file mode 100644
index 0000000..d71a751
--- /dev/null
+++ b/src/test/resources/data/issue491-valid-2.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "name": "Steve"
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-valid-3.json b/src/test/resources/data/issue491-valid-3.json
new file mode 100644
index 0000000..60d4577
--- /dev/null
+++ b/src/test/resources/data/issue491-valid-3.json
@@ -0,0 +1,7 @@
+{
+  "search": {
+    "byAge": {
+      "age": 50
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue491-valid-4.json b/src/test/resources/data/issue491-valid-4.json
new file mode 100644
index 0000000..ac8d2c8
--- /dev/null
+++ b/src/test/resources/data/issue491-valid-4.json
@@ -0,0 +1,5 @@
+{
+  "search": {
+    "age": 50
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/schema/issue491-v7.json b/src/test/resources/schema/issue491-v7.json
new file mode 100644
index 0000000..75c6292
--- /dev/null
+++ b/src/test/resources/schema/issue491-v7.json
@@ -0,0 +1,49 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://example.com/issue-470.json",
+  "title": "OneOf validation message",
+  "description": "Test description",
+  "type": "object",
+  "properties": {
+    "search": {
+      "type": "object",
+      "oneOf": [
+        {
+          "type": "object",
+          "properties": {
+            "searchAge": {
+              "type": "object",
+              "properties": {
+                "age": {
+                  "type": "integer",
+                  "maximum": 150,
+                  "minimum": 1
+                }
+              },
+              "required": [
+                "age"
+              ]
+            }
+          },
+          "required": [
+            "searchAge"
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "name": {
+              "type": "string",
+              "maxLength": 20,
+              "minLength": 1
+            }
+          },
+          "required": [
+            "name"
+          ]
+        }
+      ]
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/src/test/resources/schema/issue491_2-v7.json b/src/test/resources/schema/issue491_2-v7.json
new file mode 100644
index 0000000..a4e4951
--- /dev/null
+++ b/src/test/resources/schema/issue491_2-v7.json
@@ -0,0 +1,49 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://example.com/issue-470.json",
+  "title": "OneOf validation message",
+  "description": "Test description",
+  "type": "object",
+  "properties": {
+    "search": {
+      "type": "object",
+      "oneOf": [
+        {
+          "type": "object",
+          "properties": {
+            "byAge": {
+              "type": "object",
+              "properties": {
+                "age": {
+                  "type": "integer",
+                  "maximum": 150,
+                  "minimum": 1
+                }
+              },
+              "required": [
+                "age"
+              ]
+            }
+          },
+          "required": [
+            "byAge"
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "name": {
+              "type": "string",
+              "maxLength": 20,
+              "minLength": 1
+            }
+          },
+          "required": [
+            "name"
+          ]
+        }
+      ]
+    }
+  },
+  "additionalProperties": false
+}
diff --git a/src/test/resources/schema/issue491_3-v7.json b/src/test/resources/schema/issue491_3-v7.json
new file mode 100644
index 0000000..0d462d4
--- /dev/null
+++ b/src/test/resources/schema/issue491_3-v7.json
@@ -0,0 +1,41 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "$id": "https://example.com/issue-470.json",
+  "title": "OneOf validation message",
+  "description": "Test description",
+  "type": "object",
+  "properties": {
+    "search": {
+      "type": "object",
+      "oneOf": [
+        {
+          "type": "object",
+          "properties": {
+            "age": {
+              "type": "integer",
+              "maximum": 150,
+              "minimum": 1
+            }
+          },
+          "required": [
+            "age"
+          ]
+        },
+        {
+          "type": "object",
+          "properties": {
+            "name": {
+              "type": "string",
+              "maxLength": 20,
+              "minLength": 1
+            }
+          },
+          "required": [
+            "name"
+          ]
+        }
+      ]
+    }
+  },
+  "additionalProperties": false
+}