Skip to content

Commit

Permalink
Produces validation messages when oneOf has no valid schemas. (#720)
Browse files Browse the repository at this point in the history
Corrects #678 #516 #197

Co-authored-by: Faron Dutton <faron.dutton@insightglobal.com>
  • Loading branch information
fdutton and Faron Dutton committed Apr 20, 2023
1 parent 6cea2ca commit 284e952
Show file tree
Hide file tree
Showing 42 changed files with 965 additions and 1,065 deletions.
205 changes: 12 additions & 193 deletions src/main/java/com/networknt/schema/OneOfValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,112 +21,24 @@
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.stream.Collectors;

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

private final List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();

private static class ShortcutValidator {
private final JsonSchema schema;
private final Map<String, String> constants;

ShortcutValidator(JsonNode schemaNode, JsonSchema parentSchema,
ValidationContext validationContext, JsonSchema schema) {
JsonNode refNode = schemaNode.get(ValidatorTypeCode.REF.getValue());
JsonSchema resolvedRefSchema = refNode != null && refNode.isTextual() ? RefValidator.getRefSchema(parentSchema, validationContext, refNode.textValue()).getSchema() : null;
this.constants = extractConstants(schemaNode, resolvedRefSchema);
this.schema = schema;
}

private Map<String, String> extractConstants(JsonNode schemaNode, JsonSchema resolvedRefSchema) {
Map<String, String> refMap = resolvedRefSchema != null ? extractConstants(resolvedRefSchema.getSchemaNode()) : Collections.<String, String>emptyMap();
Map<String, String> schemaMap = extractConstants(schemaNode);
if (refMap.isEmpty()) {
return schemaMap;
}
if (schemaMap.isEmpty()) {
return refMap;
}
Map<String, String> joined = new HashMap<String, String>();
joined.putAll(schemaMap);
joined.putAll(refMap);
return joined;
}

private Map<String, String> extractConstants(JsonNode schemaNode) {
Map<String, String> result = new HashMap<String, String>();
if (!schemaNode.isObject()) {
return result;
}

JsonNode propertiesNode = schemaNode.get("properties");
if (propertiesNode == null || !propertiesNode.isObject()) {
return result;
}
Iterator<String> fit = propertiesNode.fieldNames();
while (fit.hasNext()) {
String fieldName = fit.next();
JsonNode jsonNode = propertiesNode.get(fieldName);
String constantFieldValue = getConstantFieldValue(jsonNode);
if (constantFieldValue != null && !constantFieldValue.isEmpty()) {
result.put(fieldName, constantFieldValue);
}
}
return result;
}

private String getConstantFieldValue(JsonNode jsonNode) {
if (jsonNode == null || !jsonNode.isObject() || !jsonNode.has("enum")) {
return null;
}
JsonNode enumNode = jsonNode.get("enum");
if (enumNode == null || !enumNode.isArray()) {
return null;
}
if (enumNode.size() != 1) {
return null;
}
JsonNode valueNode = enumNode.get(0);
if (valueNode == null || !valueNode.isTextual()) {
return null;
}
return valueNode.textValue();
}

public boolean allConstantsMatch(JsonNode node) {
for (Map.Entry<String, String> e : constants.entrySet()) {
JsonNode valueNode = node.get(e.getKey());
if (valueNode != null && valueNode.isTextual()) {
boolean match = e.getValue().equals(valueNode.textValue());
if (!match) {
return false;
}
}
}
return true;
}

private JsonSchema getSchema() {
return schema;
}

}
private final List<JsonSchema> schemas = new ArrayList<>();

public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext);
int size = schemaNode.size();
for (int i = 0; i < size; i++) {
JsonNode childNode = schemaNode.get(i);
JsonSchema childSchema = new JsonSchema(validationContext, schemaPath + "/" + i, parentSchema.getCurrentUri(), childNode, parentSchema);
schemas.add(new ShortcutValidator(childNode, parentSchema, validationContext, childSchema));
schemas.add(new JsonSchema(validationContext, schemaPath + "/" + i, parentSchema.getCurrentUri(), childNode, parentSchema));
}
parseErrorCode(getValidatorType().getErrorCodeKey());
}

public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
Set<ValidationMessage> errors = new LinkedHashSet<>();

// As oneOf might contain multiple schemas take a backup of evaluatedProperties.
Set<String> backupEvaluatedProperties = CollectorContext.getInstance().copyEvaluatedProperties();
Expand All @@ -143,28 +55,13 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
state.setComplexValidator(true);

int numberOfValidSchema = 0;
Set<ValidationMessage> childErrors = new LinkedHashSet<ValidationMessage>();
// validate that only a single element has been received in the oneOf node
// validation should not continue, as it contradicts the oneOf requirement of only one
// if(node.isObject() && node.size()>1) {
// errors = Collections.singleton(buildValidationMessage(at, ""));
// return Collections.unmodifiableSet(errors);
// }
Set<ValidationMessage> childErrors = new LinkedHashSet<>();

for (ShortcutValidator validator : schemas) {
for (JsonSchema schema : schemas) {
Set<ValidationMessage> schemaErrors = null;
// Reset state in case the previous validator did not match
state.setMatchedNode(true);

//This prevents from collecting all the error messages in proper format.
/* if (!validator.allConstantsMatch(node)) {
// take a shortcut: if there is any constant that does not match,
// we can bail out of the validation
continue;
}*/

// get the current validator
JsonSchema schema = validator.schema;
if (!state.isWalkEnabled()) {
schemaErrors = schema.validate(node, rootNode, at);
} else {
Expand All @@ -188,37 +85,15 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String

childErrors.addAll(schemaErrors);
}
Set<ValidationMessage> childNotRequiredErrors = childErrors.stream().filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())).collect(Collectors.toSet());

// ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
if (numberOfValidSchema > 1) {
final ValidationMessage message = getMultiSchemasValidErrorMsg(at);
if (numberOfValidSchema != 1) {
ValidationMessage message = buildValidationMessage(at, Integer.toString(numberOfValidSchema));
if (failFast) {
throw new JsonSchemaException(message);
}
errors.add(message);
}

// ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
else if (numberOfValidSchema < 1) {
if (!childNotRequiredErrors.isEmpty()) {
childErrors = childNotRequiredErrors;
}
if (!childErrors.isEmpty()) {
if (childErrors.size() > 1) {
Set<ValidationMessage> notAdditionalPropertiesOnly = new LinkedHashSet<>(childErrors.stream()
.filter((ValidationMessage validationMessage) -> !ValidatorTypeCode.ADDITIONAL_PROPERTIES.getValue().equals(validationMessage.getType()))
.sorted((vm1, vm2) -> compareValidationMessages(vm1, vm2))
.collect(Collectors.toList()));
if (notAdditionalPropertiesOnly.size() > 0) {
childErrors = notAdditionalPropertiesOnly;
}
}
errors.addAll(childErrors);
}
if (failFast) {
throw new JsonSchemaException(errors.toString());
}
errors.addAll(childErrors);
}

// Make sure to signal parent handlers we matched
Expand All @@ -238,85 +113,29 @@ else if (numberOfValidSchema < 1) {
}
}

/**
* Sort <code>ValidationMessage</code> by its type
* @return
*/
private static int compareValidationMessages(ValidationMessage vm1, ValidationMessage vm2) {
// ValidationMessage's type has smaller index in the list below has high priority
final List<String> typeCodes = Arrays.asList(
ValidatorTypeCode.TYPE.getValue(),
ValidatorTypeCode.DATETIME.getValue(),
ValidatorTypeCode.UUID.getValue(),
ValidatorTypeCode.ID.getValue(),
ValidatorTypeCode.EXCLUSIVE_MAXIMUM.getValue(),
ValidatorTypeCode.EXCLUSIVE_MINIMUM.getValue(),
ValidatorTypeCode.TRUE.getValue(),
ValidatorTypeCode.FALSE.getValue(),
ValidatorTypeCode.CONST.getValue(),
ValidatorTypeCode.CONTAINS.getValue(),
ValidatorTypeCode.PROPERTYNAMES.getValue()
);

final int index1 = typeCodes.indexOf(vm1.getType());
final int index2 = typeCodes.indexOf(vm2.getType());

if (index1 >= 0) {
if (index2 >= 0) {
return Integer.compare(index1, index2);
} else {
return -1;
}
} else {
if (index2 >= 0) {
return 1;
} else {
return vm1.getCode().compareTo(vm2.getCode());
}
}
}

private void resetValidatorState() {
ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
state.setComplexValidator(false);
state.setMatchedNode(true);
}

public List<JsonSchema> getChildSchemas() {
List<JsonSchema> childJsonSchemas = new ArrayList<JsonSchema>();
for (ShortcutValidator shortcutValidator: schemas ) {
childJsonSchemas.add(shortcutValidator.getSchema());
}
return childJsonSchemas;
}

@Override
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
HashSet<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
if (shouldValidateSchema) {
validationMessages.addAll(validate(node, rootNode, at));
} else {
for (ShortcutValidator validator : schemas) {
validator.schema.walk(node, rootNode, at , shouldValidateSchema);
for (JsonSchema schema : schemas) {
schema.walk(node, rootNode, at, shouldValidateSchema);
}
}
return validationMessages;
}

private ValidationMessage getMultiSchemasValidErrorMsg(String at){
String msg="";
for(ShortcutValidator schema: schemas){
String schemaValue = schema.getSchema().getSchemaNode().toString();
msg = msg.concat(schemaValue);
}

return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF, at, schemaPath, msg);
}

@Override
public void preloadJsonSchema() {
for (final ShortcutValidator scValidator: schemas) {
scValidator.getSchema().initializeValidators();
for (JsonSchema schema: schemas) {
schema.initializeValidators();
}
}
}
2 changes: 1 addition & 1 deletion src/main/resources/jsv-messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ minimum = {0}: must have a minimum value of {1}
multipleOf = {0}: must be multiple of {1}
not = {0}: should not be valid to the schema {1}
notAllowed = {0}.{1}: is not allowed but it is in the data
oneOf = {0}: should be valid to one and only one of schema, but more than one are valid: {1}
oneOf = {0}: should be valid to one and only one schema, but {1} are valid
pattern = {0}: does not match the regex pattern {1}
patternProperties = {0}: has some error with 'pattern properties'
prefixItems = {0}[{1}]: no validator found at this index
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/jsv-messages_de.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ minimum = {0} muss mindestens den Wert {1} haben
multipleOf = {0} muss ein Vielfaches von {1} sein
not = {0} darf nicht gültig sein für das Schema {1}
notAllowed = {0}.{1} ist nicht erlaubt und darf folglich nicht auftreten
oneOf = {0} darf nur für ein einziges Schema gültig sein, aber mehr als ein Schema ist gültig: {1}
oneOf = {0} sollte für genau ein Schema gültig sein, aber {1} sind gültig
pattern = {0} stimmt nicht mit dem regulären Ausdruck {1} überein
patternProperties = {0} stimmt nicht überein mit dem Format definiert in 'pattern properties'
properties = {0}: Ein Fehler mit 'properties' ist aufgetreten
Expand Down
3 changes: 2 additions & 1 deletion src/main/resources/jsv-messages_fa_IR.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ minimum = {0}: باید حداقل {1} ویژگی داشته باشد
multipleOf = {0}: باید مضرب از {1} باشد
not = {0}: نباید برای طرحواره معتبر باشد {1}
notAllowed = {0}.{1}: مجاز نیست اما در داده ها وجود دارد
oneOf = {0}: باید برای یک و تنها یکی از طرحواره ها معتبر باشد، اما بیش از یکی معتبر است: {1}
# needs to be re-worked by a native speaker
#oneOf = {0}: باید برای یک و تنها یکی از طرحواره ها معتبر باشد، اما بیش از یکی معتبر است: {1}
pattern = {0}: با الگوی regex مطابقت ندارد {1}
patternProperties = {0}: دارای مقداری خطا با 'خواص الگو'
prefixItems = {0}[{1}]: هیچ اعتبارسنجی در این فهرست یافت نشد
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/jsv-messages_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ minimum = {0}: doit avoir une valeur minimale de {1}
multipleOf = {0}: doit être un multiple de {1}
not = {0}: ne doit pas être valide pour le schéma {1}
notAllowed = {0}.{1} n'est pas autorisé mais est dans les données
oneOf = {0}: devrait être valide pour un et un seul des schémas, mais plus d'un sont valides : {1}
oneOf = {0}: doit être valide pour un et un seul schéma, mais {1} sont valides
pattern = {0} ne correspond pas à l'expression régulière {1}
patternProperties = {0}: a des erreurs avec 'pattern properties'
properties = {0} : a une erreur avec 'properties'
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/jsv-messages_it.properties
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ minimum={0}: deve avere un valore minimo di {1}
multipleOf={0}: deve essere un multiplo di {1}
not={0}: non dovrebbe essere valido per lo schema {1}
notAllowed={0}.{1}: non è consentito ma è nel dato
oneOf={0}: dovrebbe essere valido a uno e solo uno schema, ma più di uno sono validi: {1}
oneOf={0}: dovrebbe essere valido per uno e un solo schema, ma {1} sono validi
pattern={0}: non corrisponde alla regex {1}
patternProperties={0}: ha qualche errore con ''pattern properties''
prefixItems={0}[{1}]: nessun validatore trovato a quest''indice
Expand Down
Loading

0 comments on commit 284e952

Please sign in to comment.