diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 79ee22c2d..b8697bea9 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -52,39 +52,55 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set errors = new LinkedHashSet(); + // As AllOf might contain multiple schemas take a backup of evaluatedProperties. + Object backupEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + for (JsonSchema schema : schemas) { - errors.addAll(schema.validate(node, rootNode, at)); - - if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - final Iterator arrayElements = schemaNode.elements(); - while (arrayElements.hasNext()) { - final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); - final JsonNode $ref = allOfEntry.get("$ref"); - if (null != $ref) { - final ValidationContext.DiscriminatorContext currentDiscriminatorContext = validationContext - .getCurrentDiscriminatorContext(); - if (null != currentDiscriminatorContext) { - final ObjectNode discriminator = currentDiscriminatorContext - .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); - if (null != discriminator) { - registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at); - // now we have to check whether we have hit the right target - final String discriminatorPropertyName = discriminator.get("propertyName").asText(); - final JsonNode discriminatorNode = node.get(discriminatorPropertyName); - final String discriminatorPropertyValue = discriminatorNode == null - ? null - : discriminatorNode.textValue(); - - final JsonSchema jsonSchema = parentSchema; - checkDiscriminatorMatch( - currentDiscriminatorContext, - discriminator, - discriminatorPropertyValue, - jsonSchema); + try { + errors.addAll(schema.validate(node, rootNode, at)); + + if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + final Iterator arrayElements = schemaNode.elements(); + while (arrayElements.hasNext()) { + final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); + final JsonNode $ref = allOfEntry.get("$ref"); + if (null != $ref) { + final ValidationContext.DiscriminatorContext currentDiscriminatorContext = validationContext + .getCurrentDiscriminatorContext(); + if (null != currentDiscriminatorContext) { + final ObjectNode discriminator = currentDiscriminatorContext + .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); + if (null != discriminator) { + registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at); + // now we have to check whether we have hit the right target + final String discriminatorPropertyName = discriminator.get("propertyName").asText(); + final JsonNode discriminatorNode = node.get(discriminatorPropertyName); + final String discriminatorPropertyValue = discriminatorNode == null + ? null + : discriminatorNode.textValue(); + + final JsonSchema jsonSchema = parentSchema; + checkDiscriminatorMatch( + currentDiscriminatorContext, + discriminator, + discriminatorPropertyValue, + jsonSchema); + } } } } } + } finally { + if (errors.isEmpty()) { + List backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List) backupEvaluatedProperties); + backupEvaluatedPropertiesList.addAll((List) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES)); + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList); + } else { + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedProperties); + } } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 07cfb9111..72b116fb5 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -61,6 +61,12 @@ public Set validate(JsonNode node, JsonNode rootNode, String Set allErrors = new LinkedHashSet(); String typeValidatorName = "anyOf/type"; + // As anyOf might contain multiple schemas take a backup of evaluatedProperties. + Object backupEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + try { for (JsonSchema schema : schemas) { if (schema.getValidators().containsKey(typeValidatorName)) { @@ -74,11 +80,18 @@ public Set validate(JsonNode node, JsonNode rootNode, String } Set errors = schema.validate(node, rootNode, at); if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) { + // Clear all errors. + allErrors.clear(); + // return empty errors. return errors; } else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { if (discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { errors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK)); + allErrors.addAll(errors); + } else { + // Clear all errors. + allErrors.clear(); } return errors; } @@ -95,10 +108,22 @@ public Set validate(JsonNode node, JsonNode rootNode, String if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { validationContext.leaveDiscriminatorContextImmediately(at); } + if (allErrors.isEmpty()) { + addEvaluatedProperties(backupEvaluatedProperties); + } else { + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedProperties); + } } return Collections.unmodifiableSet(allErrors); } + private void addEvaluatedProperties(Object backupEvaluatedProperties) { + // Add all the evaluated properties. + List backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List) backupEvaluatedProperties); + backupEvaluatedPropertiesList.addAll((List) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES)); + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList); + } + @Override public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { ArrayList> results = new ArrayList<>(schemas.size()); diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 908df067b..0adb25426 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -57,21 +57,73 @@ public IfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSche public Set validate(JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); + // As if-then-else might contain multiple schemas take a backup of evaluatedProperties. + Object backupEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + + Object ifEvaluatedProperties = null; + + Object thenEvaluatedProperties = null; + + Object elseEvaluatedProperties = null; + + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + Set errors = new LinkedHashSet(); boolean ifConditionPassed; try { - ifConditionPassed = ifSchema.validate(node, rootNode, at).isEmpty(); - } catch (JsonSchemaException ex) { - // When failFast is enabled, validations are thrown as exceptions. - // An exception means the condition failed - ifConditionPassed = false; - } + try { + ifConditionPassed = ifSchema.validate(node, rootNode, at).isEmpty(); + } catch (JsonSchemaException ex) { + // When failFast is enabled, validations are thrown as exceptions. + // An exception means the condition failed + ifConditionPassed = false; + } + // Evaluated Properties from if. + ifEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + + if (ifConditionPassed && thenSchema != null) { + + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + + errors.addAll(thenSchema.validate(node, rootNode, at)); + + // Collect the then evaluated properties. + thenEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); - if (ifConditionPassed && thenSchema != null) { - errors.addAll(thenSchema.validate(node, rootNode, at)); - } else if (!ifConditionPassed && elseSchema != null) { - errors.addAll(elseSchema.validate(node, rootNode, at)); + } else if (!ifConditionPassed && elseSchema != null) { + + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + + errors.addAll(elseSchema.validate(node, rootNode, at)); + + // Collect the else evaluated properties. + elseEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + } + + } finally { + if (errors.isEmpty()) { + List backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List) backupEvaluatedProperties); + + if (ifEvaluatedProperties != null) { + backupEvaluatedPropertiesList.addAll((List) ifEvaluatedProperties); + } + + if (thenEvaluatedProperties != null) { + backupEvaluatedPropertiesList.addAll((List) thenEvaluatedProperties); + } + + if (elseEvaluatedProperties != null) { + backupEvaluatedPropertiesList.addAll((List) elseEvaluatedProperties); + } + + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList); + } else { + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedProperties); + } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index c45e6286c..efa241084 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -19,14 +19,8 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -62,6 +56,10 @@ public class JsonSchema extends BaseJsonValidator { private JsonValidator requiredValidator = null; + private JsonValidator unevaluatedPropertiesValidator = null; + + WalkListenerRunner keywordWalkListenerRunner = null; + public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode schemaNode) { this(validationContext, "#", baseUri, schemaNode, null); } @@ -84,6 +82,7 @@ private JsonSchema(ValidationContext validationContext, String schemaPath, URI c this.metaSchema = validationContext.getMetaSchema(); this.currentUri = this.combineCurrentUriWithIds(currentUri, schemaNode); if (validationContext.getConfig() != null) { + keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap()); if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator"); if (null != discriminator && null != validationContext.getCurrentDiscriminatorContext()) { @@ -202,7 +201,12 @@ private Map read(JsonNode schemaNode) { JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname); String customMessage = getCustomMessage(schemaNode, pname); JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage); - if (validator != null) { + // 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)) { validators.put(getSchemaPath() + "/" + pname, validator); if (pname.equals("required")) { @@ -249,6 +253,14 @@ private JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema) { /************************ START OF VALIDATE METHODS **********************************/ + @Override + public Set validate(JsonNode node) { + Set errors = validate(node, node, AT_ROOT); + // Process UnEvaluatedProperties after all the validators are called. + errors.addAll(processUnEvaluatedProperties(node, node, AT_ROOT, true, true)); + return errors; + } + public Set validate(JsonNode jsonNode, JsonNode rootNode, String at) { SchemaValidatorsConfig config = validationContext.getConfig(); Set errors = new LinkedHashSet(); @@ -257,7 +269,6 @@ public Set validate(JsonNode jsonNode, JsonNode rootNode, Str // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(false, true); for (JsonValidator v : getValidators().values()) { - // Validate. errors.addAll(v.validate(jsonNode, rootNode, at)); } if (null != config && config.isOpenAPI3StyleDiscriminators()) { @@ -304,7 +315,7 @@ public ValidationResult validateAndCollect(JsonNode node) { * @param at String path * @return ValidationResult */ - protected ValidationResult validateAndCollect(JsonNode jsonNode, JsonNode rootNode, String at) { + private ValidationResult validateAndCollect(JsonNode jsonNode, JsonNode rootNode, String at) { try { // Get the config. SchemaValidatorsConfig config = validationContext.getConfig(); @@ -312,6 +323,8 @@ protected ValidationResult validateAndCollect(JsonNode jsonNode, JsonNode rootNo CollectorContext collectorContext = getCollectorContext(); // Validate. Set errors = validate(jsonNode, rootNode, at); + // Validate UnEvaluatedProperties after all the validators are processed. + errors.addAll(processUnEvaluatedProperties(jsonNode, rootNode, at, true, true)); // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors. if (config.doLoadCollectors()) { // Load all the data from collectors into the context. @@ -327,7 +340,7 @@ protected ValidationResult validateAndCollect(JsonNode jsonNode, JsonNode rootNo /************************ END OF VALIDATE METHODS **********************************/ - /************************ START OF WALK METHODS **********************************/ + /*********************** START OF WALK METHODS **********************************/ /** * Walk the JSON node @@ -350,6 +363,8 @@ public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { // Load all the data from collectors into the context. collectorContext.loadCollectors(); } + // Process UnEvaluatedProperties after all the validators are called. + errors.addAll(processUnEvaluatedProperties(node, node, AT_ROOT, shouldValidateSchema, false)); // Collect errors and collector context into validation result. ValidationResult validationResult = new ValidationResult(errors, collectorContext); return validationResult; @@ -358,7 +373,6 @@ public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { @Override public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { Set validationMessages = new LinkedHashSet(); - WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap()); // Walk through all the JSONWalker's. for (Entry entry : getValidators().entrySet()) { JsonSchemaWalker jsonWalker = entry.getValue(); @@ -461,4 +475,43 @@ public void initializeValidators() { } } } + + private Set processUnEvaluatedProperties(JsonNode jsonNode, JsonNode rootNode, String at, boolean shouldValidateSchema, + boolean fromValidate) { + if (unevaluatedPropertiesValidator == null) { + return Collections.emptySet(); + } + if (!fromValidate) { + Set 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); + } + } } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index cc974592e..1df336927 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -126,89 +126,114 @@ public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS } public Set validate(JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + Set errors = new LinkedHashSet(); - ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY); - // this is a complex validator, we set the flag to true - state.setComplexValidator(true); + // As oneOf might contain multiple schemas take a backup of evaluatedProperties. + Object backupEvaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); - int numberOfValidSchema = 0; - Set errors = new LinkedHashSet(); - Set childErrors = new LinkedHashSet(); - // 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 + // Make the evaluatedProperties list empty. + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + + try { + debug(logger, node, rootNode, at); + + ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY); + + // this is a complex validator, we set the flag to true + state.setComplexValidator(true); + + int numberOfValidSchema = 0; + Set childErrors = new LinkedHashSet(); + // 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); // } - for (ShortcutValidator validator : schemas) { - Set schemaErrors = null; - // Reset state in case the previous validator did not match - state.setMatchedNode(true); + for (ShortcutValidator validator : schemas) { + Set 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. + //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 { - schemaErrors = schema.walk(node, rootNode, at, state.isValidationEnabled()); - } + // get the current validator + JsonSchema schema = validator.schema; + if (!state.isWalkEnabled()) { + schemaErrors = schema.validate(node, rootNode, at); + } else { + schemaErrors = schema.walk(node, rootNode, at, state.isValidationEnabled()); + } - // check if any validation errors have occurred - if (schemaErrors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator - if (!state.hasMatchedNode()) - continue; + // check if any validation errors have occurred + if (schemaErrors.isEmpty()) { + // check whether there are no errors HOWEVER we have validated the exact validator + if (!state.hasMatchedNode()) + continue; - numberOfValidSchema++; - } - childErrors.addAll(schemaErrors); - } + numberOfValidSchema++; + } + // If the number of valid schema is greater than one, just reset the evaluated properties and break. + if (numberOfValidSchema > 1) { + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>()); + break; + } - // 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( failFast ) { - throw new JsonSchemaException(message); + childErrors.addAll(schemaErrors); } - 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 (!childErrors.isEmpty()) { - if (childErrors.size() > 1) { - Set 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; - } + + // 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 (failFast) { + throw new JsonSchemaException(message); } - errors.addAll(childErrors); + errors.add(message); } - if( failFast ){ - throw new JsonSchemaException(errors.toString()); + + // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. + else if (numberOfValidSchema < 1) { + if (!childErrors.isEmpty()) { + if (childErrors.size() > 1) { + Set 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()); + } } - } - // Make sure to signal parent handlers we matched - if (errors.isEmpty()) - state.setMatchedNode(true); + // Make sure to signal parent handlers we matched + if (errors.isEmpty()) + state.setMatchedNode(true); - // reset the ValidatorState object in the ThreadLocal - resetValidatorState(); + // reset the ValidatorState object in the ThreadLocal + resetValidatorState(); - return Collections.unmodifiableSet(errors); + return Collections.unmodifiableSet(errors); + } finally { + if (errors.isEmpty()) { + List backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List) backupEvaluatedProperties); + backupEvaluatedPropertiesList.addAll((List) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES)); + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList); + } else { + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedProperties); + } + } } /** diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 69138a719..22f3a2fb5 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -54,6 +54,7 @@ public Set validate(JsonNode node, JsonNode rootNode, String JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = node.get(entry.getKey()); if (propertyNode != null) { + addToEvaluatedProperties(at + "." + entry.getKey()); // check whether this is a complex validator. save the state boolean isComplex = state.isComplexValidator(); // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated @@ -99,6 +100,18 @@ public Set validate(JsonNode node, JsonNode rootNode, String return Collections.unmodifiableSet(errors); } + private void addToEvaluatedProperties(String propertyPath) { + Object evaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + List evaluatedPropertiesList = null; + if (evaluatedProperties == null) { + evaluatedPropertiesList = new ArrayList<>(); + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, evaluatedPropertiesList); + } else { + evaluatedPropertiesList = (List) evaluatedProperties; + } + evaluatedPropertiesList.add(propertyPath); + } + @Override public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { HashSet validationMessages = new LinkedHashSet(); diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 50950830e..b7620aeb7 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -23,9 +23,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collections; -import java.util.Iterator; -import java.util.Set; +import java.util.*; public class TypeValidator extends BaseJsonValidator implements JsonValidator { private static final String TYPE = "type"; @@ -121,9 +119,25 @@ public Set validate(JsonNode node, JsonNode rootNode, String JsonType nodeType = TypeFactory.getValueNodeType(node, validationContext.getConfig()); return Collections.singleton(buildValidationMessage(at, nodeType.toString(), schemaType.toString())); } + // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} + if (schemaPath.endsWith("additionalProperties/type")) { + addToEvaluatedProperties(at); + } return Collections.emptySet(); } + private void addToEvaluatedProperties(String propertyPath) { + Object evaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + List evaluatedPropertiesList = null; + if (evaluatedProperties == null) { + evaluatedPropertiesList = new ArrayList<>(); + CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, evaluatedPropertiesList); + } else { + evaluatedPropertiesList = (List) evaluatedProperties; + } + evaluatedPropertiesList.add(propertyPath); + } + public static boolean isInteger(String str) { if (str == null || str.equals("")) { return false; diff --git a/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java new file mode 100644 index 000000000..db6a6bcdf --- /dev/null +++ b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2016 Network New Technologies Inc. + * + * 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 com.fasterxml.jackson.databind.JsonNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; + +public class UnEvaluatedPropertiesValidator extends BaseJsonValidator implements JsonValidator { + private static final Logger logger = LoggerFactory.getLogger(UnEvaluatedPropertiesValidator.class); + public static final String EVALUATED_PROPERTIES = "com.networknt.schema.UnEvaluatedPropertiesValidator.EvaluatedProperties"; + public static final String UNEVALUATED_PROPERTIES = "com.networknt.schema.UnEvaluatedPropertiesValidator.UnevaluatedProperties"; + private JsonNode schemaNode = null; + + public UnEvaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext); + this.schemaNode = schemaNode; + } + + public Set validate(JsonNode node, JsonNode rootNode, String at) { + if (!schemaNode.isBoolean()) { + return Collections.emptySet(); + } + // Check if unevaluatedProperties is false or true. + boolean unevaluatedProperties = schemaNode.booleanValue(); + // Process all paths in the data node. + List allPaths = new ArrayList<>(); + processAllPaths(node, at, allPaths); + // Process UnEvaluated Properties. + List unEvaluatedProperties = getUnEvaluatedProperties(allPaths); + CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unEvaluatedProperties); + // Check for errors. + if (!unevaluatedProperties) { + if (!unEvaluatedProperties.isEmpty()) { + return Collections.singleton(buildValidationMessage(String.join(", ", unEvaluatedProperties))); + } + } + return Collections.emptySet(); + } + + private List getUnEvaluatedProperties(List allPaths) { + List unevaluatedProperties = new ArrayList<>(); + Object evaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES); + if (evaluatedProperties != null) { + List evaluatedPropertiesList = (List) evaluatedProperties; + allPaths.forEach(path -> { + if (!evaluatedPropertiesList.contains(path)) { + unevaluatedProperties.add(path); + } + }); + } + return unevaluatedProperties; + } + + public void processAllPaths(JsonNode node, String at, List paths) { + Iterator nodesIterator = node.fieldNames(); + while (nodesIterator.hasNext()) { + String fieldName = nodesIterator.next(); + JsonNode jsonNode = node.get(fieldName); + if (jsonNode.isObject()) { + processAllPaths(jsonNode, at + "." + fieldName, paths); + } else { + paths.add(at + "." + fieldName); + } + } + } + + @Override + public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + if (shouldValidateSchema) { + return validate(node, rootNode, at); + } + return Collections.emptySet(); + } +} diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index 6bcba7565..4fc4f68a7 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -74,7 +74,8 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc CONTAINS("contains", "1043", new MessageFormat(I18nSupport.getString("contains")), ContainsValidator.class, 14), PROPERTYNAMES("propertyNames", "1044", new MessageFormat(I18nSupport.getString("propertyNames")), PropertyNamesValidator.class, 14), DEPENDENT_REQUIRED("dependentRequired", "1045", new MessageFormat(I18nSupport.getString("dependentRequired")), DependentRequired.class, 8), // V201909 - DEPENDENT_SCHEMAS("dependentSchemas", "1046", new MessageFormat(I18nSupport.getString("dependentSchemas")), DependentSchemas.class, 8); // V201909 + DEPENDENT_SCHEMAS("dependentSchemas", "1046", new MessageFormat(I18nSupport.getString("dependentSchemas")), DependentSchemas.class, 8), // V201909 + UNEVALUATED_PROPERTIES("unevaluatedProperties","1047",new MessageFormat(I18nSupport.getString("unevaluatedProperties")),UnEvaluatedPropertiesValidator.class,14); private static Map constants = new HashMap(); private static SpecVersion specVersion = new SpecVersion(); diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 69262d22d..313f69277 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -38,3 +38,4 @@ false = Boolean schema false is not valid const = {0}: must be a constant value {1} contains = {0}: does not contain an element that passes these validations: {1} propertyNames = Property name {0} is not valid for validation: {1} +unevaluatedProperties = There are unevaluated properties at following paths {0} diff --git a/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java b/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java index eeebfe3be..5bed72c0b 100644 --- a/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import io.undertow.Undertow; import io.undertow.server.handlers.resource.FileResourceManager; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -28,20 +29,21 @@ import java.io.InputStream; import java.net.URI; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import static io.undertow.Handlers.resource; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.*; public abstract class BaseSuiteJsonSchemaTest { protected ObjectMapper mapper = new ObjectMapper(); protected JsonSchemaFactory validatorFactory; - protected static Undertow server = null; - - protected BaseSuiteJsonSchemaTest(SpecVersion.VersionFlag version) { - validatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(version)).objectMapper(mapper).build(); - } - + protected static Undertow server = null; + + protected BaseSuiteJsonSchemaTest(SpecVersion.VersionFlag version) { + validatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(version)).objectMapper(mapper).build(); + } + @BeforeAll public static void setUp() { if (server == null) { @@ -60,20 +62,20 @@ public static void tearDown() throws Exception { try { Thread.sleep(100); } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); + Thread.currentThread().interrupt(); } server.stop(); - server = null; + server = null; } } - + protected void runTestFile(String testCaseFile) throws Exception { final URI testCaseFileUri = URI.create("classpath:" + testCaseFile); InputStream in = Thread.currentThread().getContextClassLoader() .getResourceAsStream(testCaseFile); ArrayNode testCases = mapper.readValue(in, ArrayNode.class); - + final String VALIDATION_MESSAGES = "validationMessages"; for (int j = 0; j < testCases.size(); j++) { try { JsonNode testCase = testCases.get(j); @@ -91,12 +93,14 @@ protected void runTestFile(String testCaseFile) throws Exception { List errors = new ArrayList(); errors.addAll(schema.validate(node)); - + // Clear CollectorContext after every test. if (test.get("valid").asBoolean()) { if (!errors.isEmpty()) { System.out.println("---- test case failed ----"); + System.out.println("Description: " + test.get("description")); System.out.println("schema: " + schema.toString()); System.out.println("data: " + test.get("data")); + System.out.println("errors: " + errors); } assertEquals(0, errors.size()); } else { @@ -116,10 +120,44 @@ protected void runTestFile(String testCaseFile) throws Exception { } assertEquals(false, errors.isEmpty()); } + + // ExpectedValidation Messages need not be exactly same as actual errors.. the below code checks if expected validation message is subset of actual errors + ArrayNode expectedValidationMesgs = (ArrayNode) test.get(VALIDATION_MESSAGES); + if (errors.isEmpty() && expectedValidationMesgs != null && expectedValidationMesgs.size() > 0) { + System.out.println("---- test case failed ----"); + System.out.println("schema: " + schema); + System.out.println("data: " + test.get("data")); + System.out.println("Expected Validation Messages: " + expectedValidationMesgs); + fail("Expected errors but no errors encountered during validation."); + + } else if (expectedValidationMesgs != null) { + Iterator it = expectedValidationMesgs.iterator(); + while (it.hasNext()) { + boolean found = false; + String expectedMsg = it.next().textValue(); + for (ValidationMessage actualMsg : errors) { + if (StringUtils.equals(expectedMsg, actualMsg.getMessage())) { + found = true; + break; + } + } + if (!found) { + System.out.println("---- test case failed ----"); + System.out.println("schema: " + schema); + System.out.println("data: " + test.get("data")); + System.out.println("errors: " + errors); + System.out.println("validationMessages: " + expectedValidationMesgs); + fail("Expected validation message is not found in actual validation messages"); + break; + } + } + } + + CollectorContext.getInstance().reset(); } } catch (JsonSchemaException e) { throw new IllegalStateException(String.format("Current schema should not be invalid: %s", testCaseFile), e); } } } -} +} \ No newline at end of file diff --git a/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java b/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java new file mode 100644 index 000000000..cf49dd137 --- /dev/null +++ b/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java @@ -0,0 +1,18 @@ +package com.networknt.schema; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class UnevaluatedPropertiesTest extends BaseSuiteJsonSchemaTest { + + protected UnevaluatedPropertiesTest() { + super(SpecVersion.VersionFlag.V201909); + } + + + @Test + public void testWalk() throws Exception { + runTestFile("schema/unevaluatedTests/unevaluated-tests.json"); + } +} diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json new file mode 100644 index 000000000..7ab199319 --- /dev/null +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -0,0 +1,636 @@ +[ + { + "description": "schema with a $ref", + "schema": { + "title": "Person", + "type": "object", + "definitions": { + "address": { + "properties": { + "residence": { + "$ref": "#/definitions/residence", + "description": "Residence details where the person lives" + }, + "city": { + "type": "string", + "description": "City where the person lives." + }, + "street": { + "type": "string", + "description": "street where the person lives." + }, + "pinCode": { + "type": "number", + "description": "pincode of street" + } + }, + "unevaluatedProperties": false + }, + "residence": { + "flatNumber": { + "type": "string" + }, + "flatName": { + "type": "string" + }, + "landmark": { + "type": "string" + } + } + }, + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + }, + "address": { + "description": "Address of the person.", + "$ref": "#/definitions/address" + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Basic Success Test", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "address": { + "city": "Hyderabad", + "pinCode": 500025 + } + }, + "valid": true + }, + { + "description": "Unevaluated Property - Outside $ref", + "data": { + "firstName": "First Name", + "invalid": 18, + "lastName": "Last Name", + "address": { + "city": "Hyderabad", + "pinCode": 500025 + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.invalid" + ] + }, + { + "description": "Unevaluated Property - inside $ref", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "address": { + "city": "Hyderabad", + "pinCode": 500025, + "invalid": "invalid" + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.address.invalid" + ] + }, + { + "description": "Unevaluated - multiple properties", + "data": { + "invalid1": "First Name", + "age": 18, + "lastName": "Last Name", + "address": { + "city": "Hyderabad", + "pinCode": 500025, + "invalid2": "invalid" + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.invalid1, $.address.invalid2" + ] + }, + { + "description": "Inside nested $ref", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "address": { + "city": "Hyderabad", + "pinCode": 500025, + "residence": { + "invalid": "" + } + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.address.residence.invalid" + ] + } + ] + }, + { + "description": "schema with a oneOf", + "schema": { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + }, + "vehicle": { + "oneOf": [ + { + "title": "Car", + "required": [ + "wheels", + "headlights" + ], + "properties": { + "wheels": { + "type": "string" + }, + "headlights": { + "type": "string" + } + } + }, + { + "title": "Boat", + "required": [ + "pontoons" + ], + "properties": { + "pontoons": { + "type": "string" + } + } + }, + { + "title": "Plane", + "required": [ + "wings" + ], + "properties": { + "wings": { + "type": "string" + } + } + } + ] + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with oneOf and one property", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons" + } + }, + "valid": true + }, + { + "description": "Data which satisfies 2 oneOf schemas", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons", + "wings": "wings" + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.pontoons, $.vehicle.wings" + ] + }, + { + "description": "Data which satisfies 2 oneOf schemas and an invalid prop", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons", + "wings": "wings", + "invalid": "invalid" + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.pontoons, $.vehicle.wings, $.vehicle.invalid" + ] + }, + { + "description": "Data which doesn't satisfy any of oneOf schemas but having an invalid prop", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "invalid": "invalid" + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.invalid" + ] + } + ] + }, + { + "description": "schema with a anyOf", + "schema": { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + }, + "vehicle": { + "anyOf": [ + { + "title": "Car", + "required": [ + "wheels", + "headlights" + ], + "properties": { + "wheels": { + "type": "string" + }, + "headlights": { + "type": "string" + } + } + }, + { + "title": "Boat", + "required": [ + "pontoons" + ], + "properties": { + "pontoons": { + "type": "string" + } + } + }, + { + "title": "Plane", + "required": [ + "wings" + ], + "properties": { + "wings": { + "type": "string" + } + } + } + ] + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with 1 valid AnyOf", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons" + } + }, + "valid": true + }, + { + "description": "Data with 1 AnyOf and 1 unevaluated property", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons", + "unevaluated": true + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.unevaluated" + ] + }, + { + "description": "Data with just unevaluated property", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "unevaluated": true + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.unevaluated" + ] + }, + { + "description": "Data with 2 valid AnyOf and 1 unevaluated property", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "pontoons": "pontoons", + "wings": "wings", + "unevaluated": true + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.wings, $.vehicle.unevaluated" + ] + } + ] + }, + { + "description": "schema with a allOf", + "schema": { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + }, + "vehicle": { + "allOf": [ + { + "title": "Car", + "required": [ + "wheels" + ], + "properties": { + "wheels": { + "type": "string" + }, + "headlights": { + "type": "string" + } + } + }, + { + "title": "Boat", + "required": [ + "pontoons" + ], + "properties": { + "pontoons": { + "type": "string" + } + } + }, + { + "title": "Plane", + "required": [ + "wings" + ], + "properties": { + "wings": { + "type": "string" + } + } + } + ] + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with allOf", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "wheels": "wheels", + "pontoons": "pontoons", + "wings": "wings" + } + }, + "valid": true + }, + { + "description": "Data with invalid allOf and one unevaluated property", + "data": { + "firstName": "First Name", + "age": 18, + "lastName": "Last Name", + "vehicle": { + "wheels": "wheels", + "pontoons": "pontoons", + "unevaluated": true + } + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.vehicle.unevaluated" + ] + } + ] + }, + { + "description": "schema with if then and else", + "schema": { + "title": "Person", + "type": "object", + "if": { + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "firstName" + ] + }, + "then": { + "properties": { + "lastName": { + "type": "string", + "description": "The person's last name." + } + } + }, + "else": { + "properties": { + "surName": { + "type": "string", + "description": "The person's sur name." + } + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with if then and else", + "data": { + "age": 18, + "surName": "Sur Name" + }, + "valid": true + }, + { + "description": "Data - else schema with one unevaluated property", + "data": { + "age": 18, + "surName": "Sur Name", + "unevaluated": true + }, + "valid": false, + "validationMessages": [ + "There are unevaluated properties at following paths $.unevaluated" + ] + } + ] + }, + { + "description": "schema with additional properties as object", + "schema": { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": { + "properties": { + "location": { + "type": "string", + "description": "The person's location." + } + } + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with additional properties as object", + "data": { + "age": 18, + "otherProperty": { + "location": "hello" + } + }, + "valid": true + } + ] + }, + { + "description": "schema with additional properties as type", + "schema": { + "title": "Person", + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "lastName": { + "type": "string", + "description": "The person's last name." + }, + "age": { + "description": "Age in years which must be equal to or greater than zero.", + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": { + "type": "string" + }, + "unevaluatedProperties": false + }, + "tests": [ + { + "description": "Data with additional properties as type", + "data": { + "age": 18, + "otherProperty": "test" + }, + "valid": true + } + ] + } +] \ No newline at end of file