diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 2a61073a9..cc974592e 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -186,9 +186,10 @@ public Set validate(JsonNode node, JsonNode rootNode, String else if (numberOfValidSchema < 1) { if (!childErrors.isEmpty()) { if (childErrors.size() > 1) { - Set notAdditionalPropertiesOnly = childErrors.stream() + Set 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 @@ else if (numberOfValidSchema < 1) { return Collections.unmodifiableSet(errors); } + /** + * Sort ValidationMessage 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 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); 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 000000000..84fea6317 --- /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 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 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 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 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 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 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 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 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 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 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 errors = schema.validate(node); + Assertions.assertEquals(2, errors.size()); + Assertions.assertEquals(expectedError, errors.iterator().next().getMessage()); + } + + private static Stream 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 000000000..80dd10568 --- /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 000000000..29f140c72 --- /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 000000000..37878f8db --- /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 000000000..79c8d4f37 --- /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 000000000..9528725a7 --- /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 000000000..3fad4f39b --- /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 000000000..edc86d3a9 --- /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 000000000..d71a75166 --- /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 000000000..60d4577d7 --- /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 000000000..ac8d2c8d7 --- /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 000000000..75c6292b2 --- /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 000000000..a4e495180 --- /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 000000000..0d462d44b --- /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 +}