Repository: networknt/json-schema-validator
Pull request #534 (2022-03-17 13:42:40)

Adding Unevaluated properties keyword.
This PR adds unevaluatedProperties keyword implementation to networknt's json-schema-validator. Please refer to the following discussion for more details on how this keyword might work.

https://github.com/json-schema-org/json-schema-spec/issues/556

prashanthjos: @stevehu can you please have a look at this PR and give your suggestions.
stevehu: @prashanthjos Thanks a lot for the PR. It was a good read for the spec :) I understand that the keyword is introduced in 2020-12 and we don't have this version created yet. I am going to find some time to add this new version. Meanwhile, I am wondering if you could add a test case to use the standard test suite? The test case can help us to keep the code stable if someone else wants to contribute to this keyword. Here is the link to the test https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/master/tests/draft2020-12/unevaluatedProperties.json

Patch:
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index 79ee22c..b8697be 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -52,39 +52,55 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
+        // 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<JsonNode> 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<JsonNode> 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<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
+                    backupEvaluatedPropertiesList.addAll((List<String>) 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 07cfb91..72b116f 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -61,6 +61,12 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
         Set<ValidationMessage> allErrors = new LinkedHashSet<ValidationMessage>();
         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 class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
                 }
                 Set<ValidationMessage> 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 class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
             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<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
+        backupEvaluatedPropertiesList.addAll((List<String>) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES));
+        CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList);
+    }
+
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
         ArrayList<Set<ValidationMessage>> 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 908df06..0adb254 100644
--- a/src/main/java/com/networknt/schema/IfValidator.java
+++ b/src/main/java/com/networknt/schema/IfValidator.java
@@ -57,21 +57,73 @@ public class IfValidator extends BaseJsonValidator implements JsonValidator {
     public Set<ValidationMessage> 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<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
         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<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
+
+                if (ifEvaluatedProperties != null) {
+                    backupEvaluatedPropertiesList.addAll((List<String>) ifEvaluatedProperties);
+                }
+
+                if (thenEvaluatedProperties != null) {
+                    backupEvaluatedPropertiesList.addAll((List<String>) thenEvaluatedProperties);
+                }
+
+                if (elseEvaluatedProperties != null) {
+                    backupEvaluatedPropertiesList.addAll((List<String>) 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 c45e628..efa2410 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -19,14 +19,8 @@ package com.networknt.schema;
 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 @@ public class JsonSchema extends BaseJsonValidator {
         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 @@ public class JsonSchema extends BaseJsonValidator {
                 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 @@ public class JsonSchema extends BaseJsonValidator {
 
     /************************ START OF VALIDATE METHODS **********************************/
 
+    @Override
+    public Set<ValidationMessage> validate(JsonNode node) {
+        Set<ValidationMessage> 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<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, String at) {
         SchemaValidatorsConfig config = validationContext.getConfig();
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
@@ -257,7 +269,6 @@ public class JsonSchema extends BaseJsonValidator {
         // 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 class JsonSchema extends BaseJsonValidator {
      * @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 @@ public class JsonSchema extends BaseJsonValidator {
             CollectorContext collectorContext = getCollectorContext();
             // Validate.
             Set<ValidationMessage> 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 @@ public class JsonSchema extends BaseJsonValidator {
 
     /************************ END OF VALIDATE METHODS **********************************/
 
-    /************************ START OF WALK METHODS **********************************/
+    /*********************** START OF WALK METHODS **********************************/
 
     /**
      * Walk the JSON node
@@ -350,6 +363,8 @@ public class JsonSchema extends BaseJsonValidator {
             // 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 class JsonSchema extends BaseJsonValidator {
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
         Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
-        WalkListenerRunner keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap());
         // Walk through all the JSONWalker's.
         for (Entry<String, JsonValidator> entry : getValidators().entrySet()) {
             JsonSchemaWalker jsonWalker = entry.getValue();
@@ -461,4 +475,43 @@ public class JsonSchema extends BaseJsonValidator {
             }
         }
     }
+
+    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);
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index cc97459..1df3369 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -126,89 +126,114 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
-        debug(logger, node, rootNode, at);
+        Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        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<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
-        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
+        // 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<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);
 //        }
 
-        for (ShortcutValidator validator : schemas) {
-            Set<ValidationMessage> schemaErrors = null;
-            // Reset state in case the previous validator did not match
-            state.setMatchedNode(true);
+            for (ShortcutValidator validator : 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.
+                //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<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;
-                    }
+
+            // 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<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());
+                }
             }
-        }
 
-        // 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<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
+                backupEvaluatedPropertiesList.addAll((List<String>) 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 69138a7..22f3a2f 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -54,6 +54,7 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
             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 class PropertiesValidator extends BaseJsonValidator implements JsonValida
         return Collections.unmodifiableSet(errors);
     }
 
+    private void addToEvaluatedProperties(String propertyPath) {
+        Object evaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES);
+        List<String> evaluatedPropertiesList = null;
+        if (evaluatedProperties == null) {
+            evaluatedPropertiesList = new ArrayList<>();
+            CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, evaluatedPropertiesList);
+        } else {
+            evaluatedPropertiesList = (List<String>) evaluatedProperties;
+        }
+        evaluatedPropertiesList.add(propertyPath);
+    }
+
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
         HashSet<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java
index 5095083..b7620ae 100644
--- a/src/main/java/com/networknt/schema/TypeValidator.java
+++ b/src/main/java/com/networknt/schema/TypeValidator.java
@@ -23,9 +23,7 @@ import com.networknt.schema.utils.JsonNodeUtil;
 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 class TypeValidator extends BaseJsonValidator implements JsonValidator {
             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<String> evaluatedPropertiesList = null;
+        if (evaluatedProperties == null) {
+            evaluatedPropertiesList = new ArrayList<>();
+            CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, evaluatedPropertiesList);
+        } else {
+            evaluatedPropertiesList = (List<String>) 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 0000000..db6a6bc
--- /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<ValidationMessage> 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<String> allPaths = new ArrayList<>();
+        processAllPaths(node, at, allPaths);
+        // Process UnEvaluated Properties.
+        List<String> 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<String> getUnEvaluatedProperties(List<String> allPaths) {
+        List<String> unevaluatedProperties = new ArrayList<>();
+        Object evaluatedProperties = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES);
+        if (evaluatedProperties != null) {
+            List<String> evaluatedPropertiesList = (List<String>) evaluatedProperties;
+            allPaths.forEach(path -> {
+                if (!evaluatedPropertiesList.contains(path)) {
+                    unevaluatedProperties.add(path);
+                }
+            });
+        }
+        return unevaluatedProperties;
+    }
+
+    public void processAllPaths(JsonNode node, String at, List<String> paths) {
+        Iterator<String> 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<ValidationMessage> 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 6bcba75..4fc4f68 100644
--- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java
+++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
@@ -74,7 +74,8 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
     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<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
     private static SpecVersion specVersion = new SpecVersion();
diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties
index 69262d2..313f692 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}


Test patch:
diff --git a/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java b/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java
index eeebfe3..5bed72c 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.ObjectMapper;
 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.File;
 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 abstract class BaseSuiteJsonSchemaTest {
             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 @@ public abstract class BaseSuiteJsonSchemaTest {
                     List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
 
                     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 @@ public abstract class BaseSuiteJsonSchemaTest {
                         }
                         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<JsonNode> 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 0000000..cf49dd1
--- /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 0000000..7ab1993
--- /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