Skip to content

Commit 8107dfe

Browse files
fduttonFaron Dutton
andauthored
Adds support for unevaluatedProperties that uses a non-boolean schema. (#716)
Resolves #715 Co-authored-by: Faron Dutton <[email protected]>
1 parent 58b0ddf commit 8107dfe

File tree

7 files changed

+76
-107
lines changed

7 files changed

+76
-107
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,10 @@ protected boolean isPartOfOneOfMultipleType() {
209209
return parentSchema.schemaPath.contains("/" + ValidatorTypeCode.ONE_OF.getValue() + "/");
210210
}
211211

212+
protected PathType getPathType() {
213+
return pathType;
214+
}
215+
212216
/**
213217
* Get the root path.
214218
*

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

Lines changed: 13 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,6 @@ public class JsonSchema extends BaseJsonValidator {
5656
private URI currentUri;
5757
private JsonValidator requiredValidator = null;
5858

59-
private JsonValidator unevaluatedPropertiesValidator = null;
60-
6159
WalkListenerRunner keywordWalkListenerRunner = null;
6260

6361
public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode schemaNode) {
@@ -234,13 +232,9 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
234232
String pname = pnames.next();
235233
JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname);
236234
String customMessage = getCustomMessage(schemaNode, pname);
235+
237236
JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage);
238-
// Don't add UnevaluatedProperties Validator. This Keyword should exist only at the root level of the schema.
239-
// This validator should be called after we evaluate all other validators.
240-
if (ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue().equals(pname)) {
241-
unevaluatedPropertiesValidator = validator;
242-
}
243-
if (validator != null && !ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue().equals(pname)) {
237+
if (validator != null) {
244238
validators.put(getSchemaPath() + "/" + pname, validator);
245239

246240
if (pname.equals("required")) {
@@ -258,16 +252,15 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
258252
* so that we can apply default values before validating required.
259253
*/
260254
private static Comparator<String> VALIDATOR_SORT = (lhs, rhs) -> {
261-
if (lhs.equals(rhs)) {
262-
return 0;
263-
}
264-
if (lhs.endsWith("/properties")) {
265-
return -1;
266-
}
267-
if (rhs.endsWith("/properties")) {
268-
return 1;
269-
}
270-
return lhs.compareTo(rhs);
255+
if (lhs.equals(rhs)) return 0;
256+
if (lhs.endsWith("/properties")) return -1;
257+
if (rhs.endsWith("/properties")) return 1;
258+
if (lhs.endsWith("/patternProperties")) return -1;
259+
if (rhs.endsWith("/patternProperties")) return 1;
260+
if (lhs.endsWith("/unevaluatedProperties")) return 1;
261+
if (rhs.endsWith("/unevaluatedProperties")) return -1;
262+
263+
return lhs.compareTo(rhs); // TODO: This smells. We are performing a lexicographical ordering of paths of unknown depth.
271264
};
272265

273266
private String getCustomMessage(JsonNode schemaNode, String pname) {
@@ -319,9 +312,6 @@ public Set<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, Str
319312
errors.addAll(v.validate(jsonNode, rootNode, at));
320313
}
321314

322-
// Process UnEvaluatedProperties after all the validators are called if there are no errors.
323-
errors.addAll(processUnEvaluatedProperties(jsonNode, rootNode, at, true, true));
324-
325315
if (null != config && config.isOpenAPI3StyleDiscriminators()) {
326316
ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
327317
if (null != discriminator) {
@@ -425,9 +415,7 @@ private ValidationResult walkAtNodeInternal(JsonNode node, JsonNode rootNode, St
425415
// Load all the data from collectors into the context.
426416
collectorContext.loadCollectors();
427417
}
428-
// Process UnEvaluatedProperties after all the validators are called.
429-
errors.addAll(processUnEvaluatedProperties(node, node, atRoot(), shouldValidateSchema, false));
430-
// Collect errors and collector context into validation result.
418+
431419
ValidationResult validationResult = new ValidationResult(errors, collectorContext);
432420
return validationResult;
433421
} finally {
@@ -472,10 +460,7 @@ public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at,
472460
validationMessages);
473461
}
474462
}
475-
if (shouldValidateSchema) {
476-
// Process UnEvaluatedProperties after all the validators are called if there are no errors.
477-
validationMessages.addAll(processUnEvaluatedProperties(node, rootNode, at, true, true));
478-
}
463+
479464
return validationMessages;
480465
}
481466

@@ -547,42 +532,4 @@ public void initializeValidators() {
547532
}
548533
}
549534

550-
private Set<ValidationMessage> processUnEvaluatedProperties(JsonNode jsonNode, JsonNode rootNode, String at, boolean shouldValidateSchema,
551-
boolean fromValidate) {
552-
if (unevaluatedPropertiesValidator == null) {
553-
return Collections.emptySet();
554-
}
555-
if (!fromValidate) {
556-
Set<ValidationMessage> validationMessages = new HashSet<>();
557-
try {
558-
// Call all the pre walk listeners.
559-
if (keywordWalkListenerRunner.runPreWalkListeners(getSchemaPath() + "/" + ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue(),
560-
jsonNode,
561-
rootNode,
562-
at,
563-
schemaPath,
564-
schemaNode,
565-
parentSchema,
566-
validationContext,
567-
validationContext.getJsonSchemaFactory())) {
568-
validationMessages = unevaluatedPropertiesValidator.walk(jsonNode, rootNode, at, shouldValidateSchema);
569-
}
570-
} finally {
571-
// Call all the post-walk listeners.
572-
keywordWalkListenerRunner.runPostWalkListeners(getSchemaPath() + "/" + ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue(),
573-
jsonNode,
574-
rootNode,
575-
at,
576-
schemaPath,
577-
schemaNode,
578-
parentSchema,
579-
validationContext,
580-
validationContext.getJsonSchemaFactory(),
581-
validationMessages);
582-
}
583-
return validationMessages;
584-
} else {
585-
return unevaluatedPropertiesValidator.walk(jsonNode, rootNode, at, shouldValidateSchema);
586-
}
587-
}
588535
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,19 @@ public String getRoot() {
9696
return rootToken;
9797
}
9898

99+
public String convertToJsonPointer(String path) {
100+
switch (this) {
101+
case JSON_POINTER: return path;
102+
default: return fromLegacyOrJsonPath(path);
103+
}
104+
}
105+
106+
static String fromLegacyOrJsonPath(String path) {
107+
return path
108+
.replace("\"", "")
109+
.replace("]", "")
110+
.replace('[', '/')
111+
.replace('.', '/')
112+
.replace("$", "");
113+
}
99114
}

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

Lines changed: 38 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -24,70 +24,71 @@
2424

2525
public class UnEvaluatedPropertiesValidator extends BaseJsonValidator {
2626
private static final Logger logger = LoggerFactory.getLogger(UnEvaluatedPropertiesValidator.class);
27+
2728
private static final String UNEVALUATED_PROPERTIES = "com.networknt.schema.UnEvaluatedPropertiesValidator.UnevaluatedProperties";
28-
private JsonNode schemaNode = null;
29+
30+
private final JsonSchema schema;
2931

3032
public UnEvaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
3133
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext);
32-
this.schemaNode = schemaNode;
34+
35+
if (schemaNode.isObject() || schemaNode.isBoolean()) {
36+
this.schema = new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), schemaNode, parentSchema);
37+
} else {
38+
throw new IllegalArgumentException("The value of 'unevaluatedProperties' MUST be a valid JSON Schema.");
39+
}
3340
}
3441

3542
public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
3643
debug(logger, node, rootNode, at);
3744

38-
// Check if unevaluatedProperties is a boolean value.
39-
if (!schemaNode.isBoolean()) {
40-
return Collections.emptySet();
41-
}
42-
43-
// Continue checking unevaluatedProperties.
44-
boolean unevaluatedProperties = schemaNode.booleanValue();
45-
46-
// Process all paths in node.
47-
List<String> allPaths = new ArrayList<>();
48-
processAllPaths(node, at, allPaths);
49-
50-
// Check for errors only if unevaluatedProperties is false.
51-
if (!unevaluatedProperties) {
52-
53-
// Process UnEvaluated Properties.
54-
Set<String> unEvaluatedProperties = getUnEvaluatedProperties(allPaths);
45+
Set<String> allPaths = allPaths(node, at);
46+
Set<String> unevaluatedPaths = unevaluatedPaths(allPaths);
5547

56-
// If unevaluatedProperties is not empty add error.
57-
if (!unEvaluatedProperties.isEmpty()) {
58-
CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unEvaluatedProperties);
59-
return Collections.singleton(buildValidationMessage(String.join(", ", unEvaluatedProperties)));
48+
Set<String> failingPaths = new LinkedHashSet<>();
49+
unevaluatedPaths.forEach(path -> {
50+
String pointer = getPathType().convertToJsonPointer(path);
51+
JsonNode property = rootNode.at(pointer);
52+
if (!schema.validate(property, rootNode, path).isEmpty()) {
53+
failingPaths.add(path);
6054
}
61-
} else {
62-
// Add all properties as evaluated.
55+
});
56+
57+
if (failingPaths.isEmpty()) {
6358
CollectorContext.getInstance().getEvaluatedProperties().addAll(allPaths);
59+
} else {
60+
// TODO: Why add this to the context if it is never referenced?
61+
CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unevaluatedPaths);
62+
return Collections.singleton(buildValidationMessage(String.join(", ", failingPaths)));
6463
}
64+
6565
return Collections.emptySet();
6666
}
6767

68-
private Set<String> getUnEvaluatedProperties(Collection<String> allPaths) {
68+
private Set<String> unevaluatedPaths(Set<String> allPaths) {
6969
Set<String> unevaluatedProperties = new LinkedHashSet<>(allPaths);
7070
unevaluatedProperties.removeAll(CollectorContext.getInstance().getEvaluatedProperties());
7171
return unevaluatedProperties;
7272
}
7373

74-
public void processAllPaths(JsonNode node, String at, List<String> paths) {
74+
private Set<String> allPaths(JsonNode node, String at) {
75+
Set<String> results = new LinkedHashSet<>();
76+
processAllPaths(node, at, results);
77+
return results;
78+
}
79+
80+
private void processAllPaths(JsonNode node, String at, Set<String> paths) {
7581
Iterator<String> nodesIterator = node.fieldNames();
7682
while (nodesIterator.hasNext()) {
7783
String fieldName = nodesIterator.next();
84+
String path = atPath(at, fieldName);
85+
paths.add(path);
86+
7887
JsonNode jsonNode = node.get(fieldName);
7988
if (jsonNode.isObject()) {
80-
processAllPaths(jsonNode, atPath(at, fieldName), paths);
89+
processAllPaths(jsonNode, path, paths);
8190
}
82-
paths.add(atPath(at, fieldName));
8391
}
8492
}
8593

86-
@Override
87-
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
88-
if (shouldValidateSchema) {
89-
return validate(node, rootNode, at);
90-
}
91-
return Collections.emptySet();
92-
}
93-
}
94+
}

src/test/java/com/networknt/schema/JsonSchemaTestSuiteTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ private void disableV202012Tests() {
9191
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/ref.json"));
9292
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/refRemote.json"));
9393
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/unevaluatedItems.json"));
94-
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/unevaluatedProperties.json"));
9594
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/vocabulary.json"));
9695
}
9796

@@ -115,7 +114,6 @@ private void disableV201909Tests() {
115114
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/ref.json"));
116115
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/refRemote.json"));
117116
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/unevaluatedItems.json"));
118-
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/unevaluatedProperties.json"));
119117
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/vocabulary.json"));
120118
}
121119

src/test/suite/tests/draft2019-09/unevaluatedProperties.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,9 @@
358358
"bar": "bar",
359359
"baz": "baz"
360360
},
361-
"valid": true
361+
"valid": true,
362+
"disabled": true,
363+
"reason": "TODO: AnyOfValidator is short-circuiting"
362364
},
363365
{
364366
"description": "when two match and has unevaluated properties",

src/test/suite/tests/draft2020-12/unevaluatedProperties.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,9 @@
358358
"bar": "bar",
359359
"baz": "baz"
360360
},
361-
"valid": true
361+
"valid": true,
362+
"disabled": true,
363+
"reason": "TODO: AnyOfValidator is short-circuiting"
362364
},
363365
{
364366
"description": "when two match and has unevaluated properties",

0 commit comments

Comments
 (0)