From 2795d79d170d433e33629f7d244ee466a98298fd Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:40:54 +0800 Subject: [PATCH] Fix for required annotations for evaluation not collected (#944) * Fix for required annotations for evaluation not collected due to cross-draft scenario * Fix incorrect access modifier on setAnnotationCollectionEnabled * Fix reporting for unevaluated properties --- README.md | 4 +- .../schema/AbstractJsonValidator.java | 2 +- .../networknt/schema/BaseJsonValidator.java | 40 +++++---- .../com/networknt/schema/ExecutionConfig.java | 20 ++--- .../schema/UnevaluatedItemsValidator.java | 5 +- .../UnevaluatedPropertiesValidator.java | 4 +- .../HierarchicalOutputUnitFormatter.java | 4 +- .../output/ListOutputUnitFormatter.java | 6 +- .../networknt/schema/output/OutputUnit.java | 6 +- .../schema/output/OutputUnitData.java | 27 ++++-- src/main/resources/jsv-messages.properties | 4 +- .../schema/ContentSchemaValidatorTest.java | 6 +- .../com/networknt/schema/Issue943Test.java | 84 +++++++++++++++++++ .../com/networknt/schema/OutputUnitTest.java | 73 ++++++++++++++-- .../unevaluatedTests/unevaluated-tests.json | 34 ++++---- 15 files changed, 246 insertions(+), 73 deletions(-) create mode 100644 src/test/java/com/networknt/schema/Issue943Test.java diff --git a/README.md b/README.md index c332e39b3..bef824e57 100644 --- a/README.md +++ b/README.md @@ -361,7 +361,7 @@ JsonSchema schema = factory.getSchema(SchemaLocation.of("https://json-schema.org OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionContext.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); ``` The following is sample output from the Hierarchical format. @@ -438,7 +438,7 @@ The following is sample output from the Hierarchical format. | Name | Description | Default Value |--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- | `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` -| `annotationCollectionPredicate`| The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` +| `annotationCollectionFilter` | The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` | `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` | `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaValidatorsConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` | `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index b0552a6ff..8dff5eb04 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -93,7 +93,7 @@ protected boolean collectAnnotations(ExecutionContext executionContext) { */ protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() - && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + && executionContext.getExecutionConfig().getAnnotationCollectionFilter().test(keyword); } /** diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 8419ea3b3..f645a7eef 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -315,27 +315,37 @@ public String toString() { return getEvaluationPath().getName(-1); } + /** + * Determines if the keyword exists adjacent in the evaluation path. + *

+ * This does not check if the keyword exists in the current meta schema as this + * can be a cross-draft case where the properties keyword is in a Draft 7 schema + * and the unevaluatedProperties keyword is in an outer Draft 2020-12 schema. + *

+ * The fact that the validator exists in the evaluation path implies that the + * keyword was valid in whatever meta schema for that schema it was created for. + * + * @param keyword the keyword to check + * @return true if found + */ protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { - boolean hasValidator = validationContext.getMetaSchema().getKeywords() - .get(keyword) != null; - if (hasValidator) { - JsonSchema schema = getEvaluationParentSchema(); - while (schema != null) { - for (JsonValidator validator : schema.getValidators()) { - if (keyword.equals(validator.getKeyword())) { - hasValidator = true; - break; - } - } - if (hasValidator) { + boolean hasValidator = false; + JsonSchema schema = getEvaluationParentSchema(); + while (schema != null) { + for (JsonValidator validator : schema.getValidators()) { + if (keyword.equals(validator.getKeyword())) { + hasValidator = true; break; } - schema = schema.getEvaluationParentSchema(); } + if (hasValidator) { + break; + } + schema = schema.getEvaluationParentSchema(); } return hasValidator; } - + @Override protected MessageSourceValidationMessage.Builder message() { return super.message().schemaNode(this.schemaNode); @@ -360,7 +370,7 @@ protected boolean collectAnnotations(ExecutionContext executionContext) { */ protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() - && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + && executionContext.getExecutionConfig().getAnnotationCollectionFilter().test(keyword); } /** diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index f1a04569b..e7a8aacb2 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -43,7 +43,7 @@ public class ExecutionConfig { * This does not affect annotation collection required for evaluating keywords * such as unevaluatedItems or unevaluatedProperties and only affects reporting. */ - private Predicate annotationCollectionPredicate = keyword -> false; + private Predicate annotationCollectionFilter = keyword -> false; /** * Since Draft 2019-09 format assertions are not enabled by default. @@ -126,12 +126,12 @@ public void setFailFast(boolean failFast) { * * @return if annotation collection is enabled */ - protected boolean isAnnotationCollectionEnabled() { + public boolean isAnnotationCollectionEnabled() { return annotationCollectionEnabled; } /** - * Sets whether to annotation collection is enabled. + * Sets whether the annotation collection is enabled. *

* This does not affect annotation collection required for evaluating keywords * such as unevaluatedItems or unevaluatedProperties and only affects reporting. @@ -141,7 +141,7 @@ protected boolean isAnnotationCollectionEnabled() { * * @param annotationCollectionEnabled true to enable annotation collection */ - protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) { + public void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) { this.annotationCollectionEnabled = annotationCollectionEnabled; } @@ -159,8 +159,8 @@ protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnable * @return the predicate to determine if annotation collection is allowed for * the keyword */ - public Predicate getAnnotationCollectionPredicate() { - return annotationCollectionPredicate; + public Predicate getAnnotationCollectionFilter() { + return annotationCollectionFilter; } /** @@ -173,11 +173,11 @@ public Predicate getAnnotationCollectionPredicate() { * This does not affect annotation collection required for evaluating keywords * such as unevaluatedItems or unevaluatedProperties and only affects reporting. * - * @param annotationCollectionPredicate the predicate accepting the keyword + * @param annotationCollectionFilter the predicate accepting the keyword */ - public void setAnnotationCollectionPredicate(Predicate annotationCollectionPredicate) { - this.annotationCollectionPredicate = Objects.requireNonNull(annotationCollectionPredicate, - "annotationCollectionPredicate must not be null"); + public void setAnnotationCollectionFilter(Predicate annotationCollectionFilter) { + this.annotationCollectionFilter = Objects.requireNonNull(annotationCollectionFilter, + "annotationCollectionFilter must not be null"); } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index b75881b83..784ecaa90 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -185,10 +185,11 @@ public Set validate(ExecutionContext executionContext, JsonNo if (messages.isEmpty()) { valid = true; } else { - // Report these as unevaluated paths or not matching the unevalutedItems schema + // Report these as unevaluated paths or not matching the unevaluatedItems schema messages = messages.stream() - .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .map(m -> message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(m.getInstanceLocation().getName(-1)) .failFast(executionContext.getExecutionConfig().isFailFast()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 441afa3ad..c32597854 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -128,8 +128,10 @@ public Set validate(ExecutionContext executionContext, JsonNo // Report these as unevaluated paths or not matching the unevaluatedProperties // schema messages = messages.stream() - .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .map(m -> message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(m.getInstanceLocation().getName(-1)) + .property(m.getInstanceLocation().getName(-1)) .failFast(executionContext.getExecutionConfig().isFailFast()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java index 51950baec..d69c62013 100644 --- a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java @@ -47,7 +47,7 @@ public static OutputUnit format(JsonSchema jsonSchema, Set va OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); Map valid = data.getValid(); - Map> errors = data.getErrors(); + Map> errors = data.getErrors(); Map> annotations = data.getAnnotations(); Map> droppedAnnotations = data.getDroppedAnnotations(); @@ -66,7 +66,7 @@ public static OutputUnit format(JsonSchema jsonSchema, Set va droppedAnnotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); // Process all the data - for (Entry> error : errors.entrySet()) { + for (Entry> error : errors.entrySet()) { OutputUnitKey key = error.getKey(); OutputUnit unit = index.get(key.getEvaluationPath()); unit.setInstanceLocation(key.getInstanceLocation().toString()); diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java index 009048588..290a1a52e 100644 --- a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java @@ -38,7 +38,7 @@ public static OutputUnit format(Set validationMessages, Execu OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); Map valid = data.getValid(); - Map> errors = data.getErrors(); + Map> errors = data.getErrors(); Map> annotations = data.getAnnotations(); Map> droppedAnnotations = data.getDroppedAnnotations(); @@ -52,12 +52,12 @@ public static OutputUnit format(Set validationMessages, Execu output.setInstanceLocation(key.getInstanceLocation().toString()); // Errors - Map errorMap = errors.get(key); + Map errorMap = errors.get(key); if (errorMap != null && !errorMap.isEmpty()) { if (output.getErrors() == null) { output.setErrors(new LinkedHashMap<>()); } - for (Entry errorEntry : errorMap.entrySet()) { + for (Entry errorEntry : errorMap.entrySet()) { output.getErrors().put(errorEntry.getKey(), errorEntry.getValue()); } } diff --git a/src/main/java/com/networknt/schema/output/OutputUnit.java b/src/main/java/com/networknt/schema/output/OutputUnit.java index 7f0dac946..fa63534b4 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnit.java +++ b/src/main/java/com/networknt/schema/output/OutputUnit.java @@ -43,7 +43,7 @@ public class OutputUnit { private String schemaLocation = null; private String instanceLocation = null; - private Map errors = null; + private Map errors = null; private Map annotations = null; @@ -83,11 +83,11 @@ public void setInstanceLocation(String instanceLocation) { this.instanceLocation = instanceLocation; } - public Map getErrors() { + public Map getErrors() { return errors; } - public void setErrors(Map errors) { + public void setErrors(Map errors) { this.errors = errors; } diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java index b6ab108ce..e52d3a10a 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnitData.java +++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java @@ -15,6 +15,7 @@ */ package com.networknt.schema.output; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,7 +31,7 @@ */ public class OutputUnitData { private final Map valid = new LinkedHashMap<>(); - private final Map> errors = new LinkedHashMap<>(); + private final Map> errors = new LinkedHashMap<>(); private final Map> annotations = new LinkedHashMap<>(); private final Map> droppedAnnotations = new LinkedHashMap<>(); @@ -38,7 +39,7 @@ public Map getValid() { return valid; } - public Map> getErrors() { + public Map> getErrors() { return errors; } @@ -66,11 +67,12 @@ public static String formatMessage(String message) { return message; } + @SuppressWarnings("unchecked") public static OutputUnitData from(Set validationMessages, ExecutionContext executionContext) { OutputUnitData data = new OutputUnitData(); Map valid = data.valid; - Map> errors = data.errors; + Map> errors = data.errors; Map> annotations = data.annotations; Map> droppedAnnotations = data.droppedAnnotations; @@ -80,15 +82,28 @@ public static OutputUnitData from(Set validationMessages, Exe OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent(), assertionSchemaLocation, assertion.getInstanceLocation()); valid.put(key, false); - Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); - errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); + Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); + Object value = errorMap.get(assertion.getType()); + if (value == null) { + errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); + } else { + // Existing error, make it into a list + if (value instanceof List) { + ((List) value).add(formatMessage(assertion.getMessage())); + } else { + List values = new ArrayList<>(); + values.add(value.toString()); + values.add(formatMessage(assertion.getMessage())); + errorMap.put(assertion.getType(), values); + } + } } for (List annotationsResult : executionContext.getAnnotations().asMap().values()) { for (JsonNodeAnnotation annotation : annotationsResult) { // As some annotations are required for computation, filter those that are not // required for reporting - if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate() + if (executionContext.getExecutionConfig().getAnnotationCollectionFilter() .test(annotation.getKeyword())) { SchemaLocation annotationSchemaLocation = new SchemaLocation( annotation.getSchemaLocation().getAbsoluteIri(), diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 52819b4f4..82ec3c927 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -42,8 +42,8 @@ propertyNames = Property name {0} is not valid for validation: {1} readOnly = {0}: is a readonly field, it cannot be changed required = {0}: required property ''{1}'' not found type = {0}: {1} found, {2} expected -unevaluatedItems = {0}: must not have unevaluated items or must match unevaluated items schema -unevaluatedProperties = {0}: must not have unevaluated properties +unevaluatedItems = {0}: item ''{1}'' must not be unevaluated or must match unevaluated items schema +unevaluatedProperties = {0}: property ''{1}'' must not be unevaluated unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique uuid = {0}: {1} is an invalid {2} diff --git a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java index b0d16c68b..d5f4607b2 100644 --- a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java @@ -58,12 +58,12 @@ void annotationCollection() throws JsonProcessingException { SchemaValidatorsConfig config = new SchemaValidatorsConfig(); config.setPathType(PathType.JSON_POINTER); JsonSchema schema = factory.getSchema(schemaData, config); - + String inputData = "\"helloworld\""; - + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); String expected = "{\"valid\":true,\"details\":[{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"annotations\":{\"contentMediaType\":\"application/jwt\",\"contentSchema\":{\"type\":\"array\",\"minItems\":2,\"prefixItems\":[{\"const\":{\"typ\":\"JWT\",\"alg\":\"HS256\"}},{\"type\":\"object\",\"required\":[\"iss\",\"exp\"],\"properties\":{\"iss\":{\"type\":\"string\"},\"exp\":{\"type\":\"integer\"}}}]}}}]}"; diff --git a/src/test/java/com/networknt/schema/Issue943Test.java b/src/test/java/com/networknt/schema/Issue943Test.java new file mode 100644 index 000000000..82fbc6a92 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue943Test.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue943Test { + @Test + void test() { + Map external = new HashMap<>(); + + String externalSchemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"$id\": \"https://www.example.org/point.json\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\r\n" + + " \"type\",\r\n" + + " \"coordinates\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"type\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"enum\": [\r\n" + + " \"Point\"\r\n" + + " ]\r\n" + + " },\r\n" + + " \"coordinates\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 2,\r\n" + + " \"items\": {\r\n" + + " \"type\": \"number\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + external.put("https://www.example.org/point.json", externalSchemaData); + + String schemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$ref\": \"https://www.example.org/point.json\",\r\n" + + " \"unevaluatedProperties\": false\r\n" + + "}"; + + String inputData = "{\r\n" + + " \"type\": \"Point\",\r\n" + + " \"coordinates\": [1, 1]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(external))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + assertTrue(schema.validate(inputData, InputFormat.JSON).isEmpty()); + + String badData = "{\r\n" + + " \"type\": \"Point\",\r\n" + + " \"hello\": \"Point\",\r\n" + + " \"coordinates\": [1, 1]\r\n" + + "}"; + assertFalse(schema.validate(badData, InputFormat.JSON).isEmpty()); + } +} diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java index cfa0bdf4d..1d3b83c8a 100644 --- a/src/test/java/com/networknt/schema/OutputUnitTest.java +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -20,6 +20,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -100,7 +103,7 @@ void annotationCollectionList() throws JsonProcessingException { OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); String expected = "{\"valid\":false,\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be a constant value 1\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"}},{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"}}]}"; @@ -118,7 +121,7 @@ void annotationCollectionHierarchical() throws JsonProcessingException { OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); String expected = "{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be a constant value 1\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}}]}]}"; @@ -136,7 +139,7 @@ void annotationCollectionHierarchical2() throws JsonProcessingException { OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"annotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"unspecified-prop\"]},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"annotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"annotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"annotations\":{\"title\":\"bar-prop-title\"}}]}]}"; @@ -183,7 +186,7 @@ void formatAnnotation(FormatInput formatInput) { JsonSchema schema = factory.getSchema(formatSchema, config); OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); assertTrue(outputUnit.isValid()); OutputUnit details = outputUnit.getDetails().get(0); @@ -203,7 +206,7 @@ void formatAssertion(FormatInput formatInput) { JsonSchema schema = factory.getSchema(formatSchema, config); OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); executionConfiguration.getExecutionConfig().setFormatAssertionsEnabled(true); }); assertFalse(outputUnit.isValid()); @@ -223,11 +226,69 @@ void typeUnion() { JsonSchema schema = factory.getSchema(typeSchema, config); OutputUnit outputUnit = schema.validate("1", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); }); assertFalse(outputUnit.isValid()); OutputUnit details = outputUnit.getDetails().get(0); assertNotNull(details.getErrors().get("type")); } + + @Test + void unevaluatedProperties() throws JsonProcessingException { + Map external = new HashMap<>(); + + String externalSchemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"$id\": \"https://www.example.org/point.json\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\r\n" + + " \"type\",\r\n" + + " \"coordinates\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"type\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"enum\": [\r\n" + + " \"Point\"\r\n" + + " ]\r\n" + + " },\r\n" + + " \"coordinates\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 2,\r\n" + + " \"items\": {\r\n" + + " \"type\": \"number\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + external.put("https://www.example.org/point.json", externalSchemaData); + + String schemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$ref\": \"https://www.example.org/point.json\",\r\n" + + " \"unevaluatedProperties\": false\r\n" + + "}"; + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(external))); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + // The following checks if the heirarchical output format is correct with multiple unevaluated properties + String inputData = "{\r\n" + + " \"type\": \"Point\",\r\n" + + " \"hello\": \"Point\",\r\n" + + " \"world\": \"Point\",\r\n" + + " \"coordinates\": [1, 1]\r\n" + + "}"; + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, + executionContext -> executionContext.getExecutionConfig() + .setAnnotationCollectionFilter(keyword -> true)); + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"errors\":{\"unevaluatedProperties\":[\"property 'hello' must not be unevaluated\",\"property 'world' must not be unevaluated\"]},\"droppedAnnotations\":{\"unevaluatedProperties\":[\"hello\",\"world\"]},\"details\":[{\"valid\":false,\"evaluationPath\":\"/$ref\",\"schemaLocation\":\"https://www.example.org/point.json#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"type\",\"coordinates\"]}}]}"; + assertEquals(expected, output); + } } diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index b5b3baab2..fdd7dc148 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -89,7 +89,7 @@ }, "valid": false, "validationMessages": [ - "$.invalid: must not have unevaluated properties" + "$: property 'invalid' must not be unevaluated" ] }, { @@ -106,7 +106,7 @@ }, "valid": false, "validationMessages": [ - "$.address.invalid: must not have unevaluated properties" + "$.address: property 'invalid' must not be unevaluated" ] }, { @@ -123,7 +123,7 @@ }, "valid": false, "validationMessages": [ - "$.address.invalid2: must not have unevaluated properties" + "$.address: property 'invalid2' must not be unevaluated" ] }, { @@ -142,7 +142,7 @@ }, "valid": false, "validationMessages": [ - "$.address.residence.invalid: must not have unevaluated properties" + "$.address.residence: property 'invalid' must not be unevaluated" ] } ] @@ -237,7 +237,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.wheels: must not have unevaluated properties" + "$.vehicle: property 'wheels' must not be unevaluated" ] }, { @@ -253,8 +253,8 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.pontoons: must not have unevaluated properties", - "$.vehicle.wings: must not have unevaluated properties" + "$.vehicle: property 'pontoons' must not be unevaluated", + "$.vehicle: property 'wings' must not be unevaluated" ] }, { @@ -271,9 +271,9 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.invalid: must not have unevaluated properties", - "$.vehicle.pontoons: must not have unevaluated properties", - "$.vehicle.wings: must not have unevaluated properties" + "$.vehicle: property 'invalid' must not be unevaluated", + "$.vehicle: property 'pontoons' must not be unevaluated", + "$.vehicle: property 'wings' must not be unevaluated" ] }, { @@ -288,7 +288,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.invalid: must not have unevaluated properties" + "$.vehicle: property 'invalid' must not be unevaluated" ] } ] @@ -383,7 +383,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.unevaluated: must not have unevaluated properties" + "$.vehicle: property 'unevaluated' must not be unevaluated" ] }, { @@ -398,7 +398,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.unevaluated: must not have unevaluated properties" + "$.vehicle: property 'unevaluated' must not be unevaluated" ] }, { @@ -415,7 +415,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.unevaluated: must not have unevaluated properties" + "$.vehicle: property 'unevaluated' must not be unevaluated" ] } ] @@ -513,7 +513,7 @@ "valid": false, "validationMessages": [ "$.vehicle: required property 'wings' not found", - "$.vehicle.unevaluated: must not have unevaluated properties" + "$.vehicle: property 'unevaluated' must not be unevaluated" ] } ] @@ -575,8 +575,8 @@ }, "valid": false, "validationMessages": [ - "$.age: must not have unevaluated properties", - "$.unevaluated: must not have unevaluated properties" + "$: property 'age' must not be unevaluated", + "$: property 'unevaluated' must not be unevaluated" ] } ]