Skip to content

Commit

Permalink
Adds support for unevaluatedProperties that uses a non-boolean schema. (
Browse files Browse the repository at this point in the history
#716)

Resolves #715

Co-authored-by: Faron Dutton <faron.dutton@insightglobal.com>
  • Loading branch information
fdutton and Faron Dutton committed Apr 18, 2023
1 parent 58b0ddf commit 8107dfe
Show file tree
Hide file tree
Showing 7 changed files with 76 additions and 107 deletions.
4 changes: 4 additions & 0 deletions src/main/java/com/networknt/schema/BaseJsonValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,10 @@ protected boolean isPartOfOneOfMultipleType() {
return parentSchema.schemaPath.contains("/" + ValidatorTypeCode.ONE_OF.getValue() + "/");
}

protected PathType getPathType() {
return pathType;
}

/**
* Get the root path.
*
Expand Down
79 changes: 13 additions & 66 deletions src/main/java/com/networknt/schema/JsonSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ public class JsonSchema extends BaseJsonValidator {
private URI currentUri;
private JsonValidator requiredValidator = null;

private JsonValidator unevaluatedPropertiesValidator = null;

WalkListenerRunner keywordWalkListenerRunner = null;

public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode schemaNode) {
Expand Down Expand Up @@ -234,13 +232,9 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
String pname = pnames.next();
JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname);
String customMessage = getCustomMessage(schemaNode, pname);

JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage);
// Don't add UnevaluatedProperties Validator. This Keyword should exist only at the root level of the schema.
// This validator should be called after we evaluate all other validators.
if (ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue().equals(pname)) {
unevaluatedPropertiesValidator = validator;
}
if (validator != null && !ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue().equals(pname)) {
if (validator != null) {
validators.put(getSchemaPath() + "/" + pname, validator);

if (pname.equals("required")) {
Expand All @@ -258,16 +252,15 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
* so that we can apply default values before validating required.
*/
private static Comparator<String> VALIDATOR_SORT = (lhs, rhs) -> {
if (lhs.equals(rhs)) {
return 0;
}
if (lhs.endsWith("/properties")) {
return -1;
}
if (rhs.endsWith("/properties")) {
return 1;
}
return lhs.compareTo(rhs);
if (lhs.equals(rhs)) return 0;
if (lhs.endsWith("/properties")) return -1;
if (rhs.endsWith("/properties")) return 1;
if (lhs.endsWith("/patternProperties")) return -1;
if (rhs.endsWith("/patternProperties")) return 1;
if (lhs.endsWith("/unevaluatedProperties")) return 1;
if (rhs.endsWith("/unevaluatedProperties")) return -1;

return lhs.compareTo(rhs); // TODO: This smells. We are performing a lexicographical ordering of paths of unknown depth.
};

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

// Process UnEvaluatedProperties after all the validators are called if there are no errors.
errors.addAll(processUnEvaluatedProperties(jsonNode, rootNode, at, true, true));

if (null != config && config.isOpenAPI3StyleDiscriminators()) {
ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
if (null != discriminator) {
Expand Down Expand Up @@ -425,9 +415,7 @@ private ValidationResult walkAtNodeInternal(JsonNode node, JsonNode rootNode, St
// Load all the data from collectors into the context.
collectorContext.loadCollectors();
}
// Process UnEvaluatedProperties after all the validators are called.
errors.addAll(processUnEvaluatedProperties(node, node, atRoot(), shouldValidateSchema, false));
// Collect errors and collector context into validation result.

ValidationResult validationResult = new ValidationResult(errors, collectorContext);
return validationResult;
} finally {
Expand Down Expand Up @@ -472,10 +460,7 @@ public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at,
validationMessages);
}
}
if (shouldValidateSchema) {
// Process UnEvaluatedProperties after all the validators are called if there are no errors.
validationMessages.addAll(processUnEvaluatedProperties(node, rootNode, at, true, true));
}

return validationMessages;
}

Expand Down Expand Up @@ -547,42 +532,4 @@ public void initializeValidators() {
}
}

private Set<ValidationMessage> processUnEvaluatedProperties(JsonNode jsonNode, JsonNode rootNode, String at, boolean shouldValidateSchema,
boolean fromValidate) {
if (unevaluatedPropertiesValidator == null) {
return Collections.emptySet();
}
if (!fromValidate) {
Set<ValidationMessage> validationMessages = new HashSet<>();
try {
// Call all the pre walk listeners.
if (keywordWalkListenerRunner.runPreWalkListeners(getSchemaPath() + "/" + ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue(),
jsonNode,
rootNode,
at,
schemaPath,
schemaNode,
parentSchema,
validationContext,
validationContext.getJsonSchemaFactory())) {
validationMessages = unevaluatedPropertiesValidator.walk(jsonNode, rootNode, at, shouldValidateSchema);
}
} finally {
// Call all the post-walk listeners.
keywordWalkListenerRunner.runPostWalkListeners(getSchemaPath() + "/" + ValidatorTypeCode.UNEVALUATED_PROPERTIES.getValue(),
jsonNode,
rootNode,
at,
schemaPath,
schemaNode,
parentSchema,
validationContext,
validationContext.getJsonSchemaFactory(),
validationMessages);
}
return validationMessages;
} else {
return unevaluatedPropertiesValidator.walk(jsonNode, rootNode, at, shouldValidateSchema);
}
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/networknt/schema/PathType.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,19 @@ public String getRoot() {
return rootToken;
}

public String convertToJsonPointer(String path) {
switch (this) {
case JSON_POINTER: return path;
default: return fromLegacyOrJsonPath(path);
}
}

static String fromLegacyOrJsonPath(String path) {
return path
.replace("\"", "")
.replace("]", "")
.replace('[', '/')
.replace('.', '/')
.replace("$", "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,70 +24,71 @@

public class UnEvaluatedPropertiesValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(UnEvaluatedPropertiesValidator.class);

private static final String UNEVALUATED_PROPERTIES = "com.networknt.schema.UnEvaluatedPropertiesValidator.UnevaluatedProperties";
private JsonNode schemaNode = null;

private final JsonSchema schema;

public UnEvaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext);
this.schemaNode = schemaNode;

if (schemaNode.isObject() || schemaNode.isBoolean()) {
this.schema = new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), schemaNode, parentSchema);
} else {
throw new IllegalArgumentException("The value of 'unevaluatedProperties' MUST be a valid JSON Schema.");
}
}

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

// Check if unevaluatedProperties is a boolean value.
if (!schemaNode.isBoolean()) {
return Collections.emptySet();
}

// Continue checking unevaluatedProperties.
boolean unevaluatedProperties = schemaNode.booleanValue();

// Process all paths in node.
List<String> allPaths = new ArrayList<>();
processAllPaths(node, at, allPaths);

// Check for errors only if unevaluatedProperties is false.
if (!unevaluatedProperties) {

// Process UnEvaluated Properties.
Set<String> unEvaluatedProperties = getUnEvaluatedProperties(allPaths);
Set<String> allPaths = allPaths(node, at);
Set<String> unevaluatedPaths = unevaluatedPaths(allPaths);

// If unevaluatedProperties is not empty add error.
if (!unEvaluatedProperties.isEmpty()) {
CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unEvaluatedProperties);
return Collections.singleton(buildValidationMessage(String.join(", ", unEvaluatedProperties)));
Set<String> failingPaths = new LinkedHashSet<>();
unevaluatedPaths.forEach(path -> {
String pointer = getPathType().convertToJsonPointer(path);
JsonNode property = rootNode.at(pointer);
if (!schema.validate(property, rootNode, path).isEmpty()) {
failingPaths.add(path);
}
} else {
// Add all properties as evaluated.
});

if (failingPaths.isEmpty()) {
CollectorContext.getInstance().getEvaluatedProperties().addAll(allPaths);
} else {
// TODO: Why add this to the context if it is never referenced?
CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unevaluatedPaths);
return Collections.singleton(buildValidationMessage(String.join(", ", failingPaths)));
}

return Collections.emptySet();
}

private Set<String> getUnEvaluatedProperties(Collection<String> allPaths) {
private Set<String> unevaluatedPaths(Set<String> allPaths) {
Set<String> unevaluatedProperties = new LinkedHashSet<>(allPaths);
unevaluatedProperties.removeAll(CollectorContext.getInstance().getEvaluatedProperties());
return unevaluatedProperties;
}

public void processAllPaths(JsonNode node, String at, List<String> paths) {
private Set<String> allPaths(JsonNode node, String at) {
Set<String> results = new LinkedHashSet<>();
processAllPaths(node, at, results);
return results;
}

private void processAllPaths(JsonNode node, String at, Set<String> paths) {
Iterator<String> nodesIterator = node.fieldNames();
while (nodesIterator.hasNext()) {
String fieldName = nodesIterator.next();
String path = atPath(at, fieldName);
paths.add(path);

JsonNode jsonNode = node.get(fieldName);
if (jsonNode.isObject()) {
processAllPaths(jsonNode, atPath(at, fieldName), paths);
processAllPaths(jsonNode, path, paths);
}
paths.add(atPath(at, fieldName));
}
}

@Override
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
if (shouldValidateSchema) {
return validate(node, rootNode, at);
}
return Collections.emptySet();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ private void disableV202012Tests() {
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/ref.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/refRemote.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/unevaluatedItems.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/unevaluatedProperties.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2020-12/vocabulary.json"));
}

Expand All @@ -115,7 +114,6 @@ private void disableV201909Tests() {
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/ref.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/refRemote.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/unevaluatedItems.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/unevaluatedProperties.json"));
disabled.add(Paths.get("src/test/suite/tests/draft2019-09/vocabulary.json"));
}

Expand Down
4 changes: 3 additions & 1 deletion src/test/suite/tests/draft2019-09/unevaluatedProperties.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@
"bar": "bar",
"baz": "baz"
},
"valid": true
"valid": true,
"disabled": true,
"reason": "TODO: AnyOfValidator is short-circuiting"
},
{
"description": "when two match and has unevaluated properties",
Expand Down
4 changes: 3 additions & 1 deletion src/test/suite/tests/draft2020-12/unevaluatedProperties.json
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@
"bar": "bar",
"baz": "baz"
},
"valid": true
"valid": true,
"disabled": true,
"reason": "TODO: AnyOfValidator is short-circuiting"
},
{
"description": "when two match and has unevaluated properties",
Expand Down

0 comments on commit 8107dfe

Please sign in to comment.