Skip to content

Commit 2795d79

Browse files
authored
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
1 parent 49a44c9 commit 2795d79

15 files changed

+246
-73
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ JsonSchema schema = factory.getSchema(SchemaLocation.of("https://json-schema.org
361361

362362
OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> {
363363
executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true);
364-
executionContext.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true);
364+
executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
365365
});
366366
```
367367
The following is sample output from the Hierarchical format.
@@ -438,7 +438,7 @@ The following is sample output from the Hierarchical format.
438438
| Name | Description | Default Value
439439
|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------
440440
| `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false`
441-
| `annotationCollectionPredicate`| The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false`
441+
| `annotationCollectionFilter` | The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false`
442442
| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()`
443443
| `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`
444444
| `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`

src/main/java/com/networknt/schema/AbstractJsonValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ protected boolean collectAnnotations(ExecutionContext executionContext) {
9393
*/
9494
protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) {
9595
return executionContext.getExecutionConfig().isAnnotationCollectionEnabled()
96-
&& executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword);
96+
&& executionContext.getExecutionConfig().getAnnotationCollectionFilter().test(keyword);
9797
}
9898

9999
/**

src/main/java/com/networknt/schema/BaseJsonValidator.java

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -315,27 +315,37 @@ public String toString() {
315315
return getEvaluationPath().getName(-1);
316316
}
317317

318+
/**
319+
* Determines if the keyword exists adjacent in the evaluation path.
320+
* <p>
321+
* This does not check if the keyword exists in the current meta schema as this
322+
* can be a cross-draft case where the properties keyword is in a Draft 7 schema
323+
* and the unevaluatedProperties keyword is in an outer Draft 2020-12 schema.
324+
* <p>
325+
* The fact that the validator exists in the evaluation path implies that the
326+
* keyword was valid in whatever meta schema for that schema it was created for.
327+
*
328+
* @param keyword the keyword to check
329+
* @return true if found
330+
*/
318331
protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) {
319-
boolean hasValidator = validationContext.getMetaSchema().getKeywords()
320-
.get(keyword) != null;
321-
if (hasValidator) {
322-
JsonSchema schema = getEvaluationParentSchema();
323-
while (schema != null) {
324-
for (JsonValidator validator : schema.getValidators()) {
325-
if (keyword.equals(validator.getKeyword())) {
326-
hasValidator = true;
327-
break;
328-
}
329-
}
330-
if (hasValidator) {
332+
boolean hasValidator = false;
333+
JsonSchema schema = getEvaluationParentSchema();
334+
while (schema != null) {
335+
for (JsonValidator validator : schema.getValidators()) {
336+
if (keyword.equals(validator.getKeyword())) {
337+
hasValidator = true;
331338
break;
332339
}
333-
schema = schema.getEvaluationParentSchema();
334340
}
341+
if (hasValidator) {
342+
break;
343+
}
344+
schema = schema.getEvaluationParentSchema();
335345
}
336346
return hasValidator;
337347
}
338-
348+
339349
@Override
340350
protected MessageSourceValidationMessage.Builder message() {
341351
return super.message().schemaNode(this.schemaNode);
@@ -360,7 +370,7 @@ protected boolean collectAnnotations(ExecutionContext executionContext) {
360370
*/
361371
protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) {
362372
return executionContext.getExecutionConfig().isAnnotationCollectionEnabled()
363-
&& executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword);
373+
&& executionContext.getExecutionConfig().getAnnotationCollectionFilter().test(keyword);
364374
}
365375

366376
/**

src/main/java/com/networknt/schema/ExecutionConfig.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class ExecutionConfig {
4343
* This does not affect annotation collection required for evaluating keywords
4444
* such as unevaluatedItems or unevaluatedProperties and only affects reporting.
4545
*/
46-
private Predicate<String> annotationCollectionPredicate = keyword -> false;
46+
private Predicate<String> annotationCollectionFilter = keyword -> false;
4747

4848
/**
4949
* Since Draft 2019-09 format assertions are not enabled by default.
@@ -126,12 +126,12 @@ public void setFailFast(boolean failFast) {
126126
*
127127
* @return if annotation collection is enabled
128128
*/
129-
protected boolean isAnnotationCollectionEnabled() {
129+
public boolean isAnnotationCollectionEnabled() {
130130
return annotationCollectionEnabled;
131131
}
132132

133133
/**
134-
* Sets whether to annotation collection is enabled.
134+
* Sets whether the annotation collection is enabled.
135135
* <p>
136136
* This does not affect annotation collection required for evaluating keywords
137137
* such as unevaluatedItems or unevaluatedProperties and only affects reporting.
@@ -141,7 +141,7 @@ protected boolean isAnnotationCollectionEnabled() {
141141
*
142142
* @param annotationCollectionEnabled true to enable annotation collection
143143
*/
144-
protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) {
144+
public void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) {
145145
this.annotationCollectionEnabled = annotationCollectionEnabled;
146146
}
147147

@@ -159,8 +159,8 @@ protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnable
159159
* @return the predicate to determine if annotation collection is allowed for
160160
* the keyword
161161
*/
162-
public Predicate<String> getAnnotationCollectionPredicate() {
163-
return annotationCollectionPredicate;
162+
public Predicate<String> getAnnotationCollectionFilter() {
163+
return annotationCollectionFilter;
164164
}
165165

166166
/**
@@ -173,11 +173,11 @@ public Predicate<String> getAnnotationCollectionPredicate() {
173173
* This does not affect annotation collection required for evaluating keywords
174174
* such as unevaluatedItems or unevaluatedProperties and only affects reporting.
175175
*
176-
* @param annotationCollectionPredicate the predicate accepting the keyword
176+
* @param annotationCollectionFilter the predicate accepting the keyword
177177
*/
178-
public void setAnnotationCollectionPredicate(Predicate<String> annotationCollectionPredicate) {
179-
this.annotationCollectionPredicate = Objects.requireNonNull(annotationCollectionPredicate,
180-
"annotationCollectionPredicate must not be null");
178+
public void setAnnotationCollectionFilter(Predicate<String> annotationCollectionFilter) {
179+
this.annotationCollectionFilter = Objects.requireNonNull(annotationCollectionFilter,
180+
"annotationCollectionFilter must not be null");
181181
}
182182

183183
}

src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,11 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
185185
if (messages.isEmpty()) {
186186
valid = true;
187187
} else {
188-
// Report these as unevaluated paths or not matching the unevalutedItems schema
188+
// Report these as unevaluated paths or not matching the unevaluatedItems schema
189189
messages = messages.stream()
190-
.map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation())
190+
.map(m -> message().instanceNode(node).instanceLocation(instanceLocation)
191191
.locale(executionContext.getExecutionConfig().getLocale())
192+
.arguments(m.getInstanceLocation().getName(-1))
192193
.failFast(executionContext.getExecutionConfig().isFailFast()).build())
193194
.collect(Collectors.toCollection(LinkedHashSet::new));
194195
}

src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
128128
// Report these as unevaluated paths or not matching the unevaluatedProperties
129129
// schema
130130
messages = messages.stream()
131-
.map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation())
131+
.map(m -> message().instanceNode(node).instanceLocation(instanceLocation)
132132
.locale(executionContext.getExecutionConfig().getLocale())
133+
.arguments(m.getInstanceLocation().getName(-1))
134+
.property(m.getInstanceLocation().getName(-1))
133135
.failFast(executionContext.getExecutionConfig().isFailFast()).build())
134136
.collect(Collectors.toCollection(LinkedHashSet::new));
135137
}

src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> va
4747
OutputUnitData data = OutputUnitData.from(validationMessages, executionContext);
4848

4949
Map<OutputUnitKey, Boolean> valid = data.getValid();
50-
Map<OutputUnitKey, Map<String, String>> errors = data.getErrors();
50+
Map<OutputUnitKey, Map<String, Object>> errors = data.getErrors();
5151
Map<OutputUnitKey, Map<String, Object>> annotations = data.getAnnotations();
5252
Map<OutputUnitKey, Map<String, Object>> droppedAnnotations = data.getDroppedAnnotations();
5353

@@ -66,7 +66,7 @@ public static OutputUnit format(JsonSchema jsonSchema, Set<ValidationMessage> va
6666
droppedAnnotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root));
6767

6868
// Process all the data
69-
for (Entry<OutputUnitKey, Map<String, String>> error : errors.entrySet()) {
69+
for (Entry<OutputUnitKey, Map<String, Object>> error : errors.entrySet()) {
7070
OutputUnitKey key = error.getKey();
7171
OutputUnit unit = index.get(key.getEvaluationPath());
7272
unit.setInstanceLocation(key.getInstanceLocation().toString());

src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static OutputUnit format(Set<ValidationMessage> validationMessages, Execu
3838
OutputUnitData data = OutputUnitData.from(validationMessages, executionContext);
3939

4040
Map<OutputUnitKey, Boolean> valid = data.getValid();
41-
Map<OutputUnitKey, Map<String, String>> errors = data.getErrors();
41+
Map<OutputUnitKey, Map<String, Object>> errors = data.getErrors();
4242
Map<OutputUnitKey, Map<String, Object>> annotations = data.getAnnotations();
4343
Map<OutputUnitKey, Map<String, Object>> droppedAnnotations = data.getDroppedAnnotations();
4444

@@ -52,12 +52,12 @@ public static OutputUnit format(Set<ValidationMessage> validationMessages, Execu
5252
output.setInstanceLocation(key.getInstanceLocation().toString());
5353

5454
// Errors
55-
Map<String, String> errorMap = errors.get(key);
55+
Map<String, Object> errorMap = errors.get(key);
5656
if (errorMap != null && !errorMap.isEmpty()) {
5757
if (output.getErrors() == null) {
5858
output.setErrors(new LinkedHashMap<>());
5959
}
60-
for (Entry<String, String> errorEntry : errorMap.entrySet()) {
60+
for (Entry<String, Object> errorEntry : errorMap.entrySet()) {
6161
output.getErrors().put(errorEntry.getKey(), errorEntry.getValue());
6262
}
6363
}

src/main/java/com/networknt/schema/output/OutputUnit.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public class OutputUnit {
4343
private String schemaLocation = null;
4444
private String instanceLocation = null;
4545

46-
private Map<String, String> errors = null;
46+
private Map<String, Object> errors = null;
4747

4848
private Map<String, Object> annotations = null;
4949

@@ -83,11 +83,11 @@ public void setInstanceLocation(String instanceLocation) {
8383
this.instanceLocation = instanceLocation;
8484
}
8585

86-
public Map<String, String> getErrors() {
86+
public Map<String, Object> getErrors() {
8787
return errors;
8888
}
8989

90-
public void setErrors(Map<String, String> errors) {
90+
public void setErrors(Map<String, Object> errors) {
9191
this.errors = errors;
9292
}
9393

src/main/java/com/networknt/schema/output/OutputUnitData.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.networknt.schema.output;
1717

18+
import java.util.ArrayList;
1819
import java.util.LinkedHashMap;
1920
import java.util.List;
2021
import java.util.Map;
@@ -30,15 +31,15 @@
3031
*/
3132
public class OutputUnitData {
3233
private final Map<OutputUnitKey, Boolean> valid = new LinkedHashMap<>();
33-
private final Map<OutputUnitKey, Map<String, String>> errors = new LinkedHashMap<>();
34+
private final Map<OutputUnitKey, Map<String, Object>> errors = new LinkedHashMap<>();
3435
private final Map<OutputUnitKey, Map<String, Object>> annotations = new LinkedHashMap<>();
3536
private final Map<OutputUnitKey, Map<String, Object>> droppedAnnotations = new LinkedHashMap<>();
3637

3738
public Map<OutputUnitKey, Boolean> getValid() {
3839
return valid;
3940
}
4041

41-
public Map<OutputUnitKey, Map<String, String>> getErrors() {
42+
public Map<OutputUnitKey, Map<String, Object>> getErrors() {
4243
return errors;
4344
}
4445

@@ -66,11 +67,12 @@ public static String formatMessage(String message) {
6667
return message;
6768
}
6869

70+
@SuppressWarnings("unchecked")
6971
public static OutputUnitData from(Set<ValidationMessage> validationMessages, ExecutionContext executionContext) {
7072
OutputUnitData data = new OutputUnitData();
7173

7274
Map<OutputUnitKey, Boolean> valid = data.valid;
73-
Map<OutputUnitKey, Map<String, String>> errors = data.errors;
75+
Map<OutputUnitKey, Map<String, Object>> errors = data.errors;
7476
Map<OutputUnitKey, Map<String, Object>> annotations = data.annotations;
7577
Map<OutputUnitKey, Map<String, Object>> droppedAnnotations = data.droppedAnnotations;
7678

@@ -80,15 +82,28 @@ public static OutputUnitData from(Set<ValidationMessage> validationMessages, Exe
8082
OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent(),
8183
assertionSchemaLocation, assertion.getInstanceLocation());
8284
valid.put(key, false);
83-
Map<String, String> errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>());
84-
errorMap.put(assertion.getType(), formatMessage(assertion.getMessage()));
85+
Map<String, Object> errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>());
86+
Object value = errorMap.get(assertion.getType());
87+
if (value == null) {
88+
errorMap.put(assertion.getType(), formatMessage(assertion.getMessage()));
89+
} else {
90+
// Existing error, make it into a list
91+
if (value instanceof List) {
92+
((List<String>) value).add(formatMessage(assertion.getMessage()));
93+
} else {
94+
List<String> values = new ArrayList<>();
95+
values.add(value.toString());
96+
values.add(formatMessage(assertion.getMessage()));
97+
errorMap.put(assertion.getType(), values);
98+
}
99+
}
85100
}
86101

87102
for (List<JsonNodeAnnotation> annotationsResult : executionContext.getAnnotations().asMap().values()) {
88103
for (JsonNodeAnnotation annotation : annotationsResult) {
89104
// As some annotations are required for computation, filter those that are not
90105
// required for reporting
91-
if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate()
106+
if (executionContext.getExecutionConfig().getAnnotationCollectionFilter()
92107
.test(annotation.getKeyword())) {
93108
SchemaLocation annotationSchemaLocation = new SchemaLocation(
94109
annotation.getSchemaLocation().getAbsoluteIri(),

0 commit comments

Comments
 (0)