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"
]
}
]