Repository: networknt/json-schema-validator
Pull request #544 (2022-03-31 15:59:12)

Fixing unevaluated properties with larger test base
@stevehu I have made some changes to unevaluated properties. As suggested by you I have added all the tests from https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/master/tests/draft2019-09/unevaluatedProperties.json to 
json-schema-validator/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json. I dint want to read the http url from the tests, so I just copied them.

Also unevaluatedProperties implementation just accepts a boolean value and not a schema today. I have three tests failing from the above tests and remaining all are passing. I will be working on fixing those tests soon.

stevehu: @prashanthjos I think we can add CI to all the PRs and it makes sense to do the build before we merge it. At the moment, I am building it and doing the directory comparison between the PR branch and the master. BTW, I have merged one PR and the AllOfValidator has conflicts now. I am wondering if you could take a look. Thanks.
stevehu: @prashanthjos Thanks a lot for the fix. Since we have a memory leak issue in the master. I am wondering if we should wait until that issue is resolved before merging this issue. Please let me know what do you think?
prashanthjos: @stevehu looks like master build is failing because of test failures after the last merge. Can you please take a look.
stevehu: I have rolled back the other PR. Thanks.
prashanthjos: @stevehu looks like users are not realising the correct usage of CollectorContext and resetting it correctly.  So I am going to clear everything from CollectorContext for the existing walk and validate methods. I will add new validate and walk methods that will take an additional boolean parameter `resetCollectorContext` or we can add an additional attribute in SchemaValidatorConfig about resetting the CollectorContext. Please let me know your thoughts. I will update the same PR.
stevehu: @prashanthjos Most users just want to validate the JSON with the schema and they might not understand the implementation details. It is our responsibility to make the library easy to use for basic functionalities and additional features for advanced users with customized configurations. Even if we put an additional parameter, some users will have trouble understanding how to use it and they might just copy the code from others without understanding the usage of the indicator. In my opinion, it is best to add a config property and the default is to reset the collector context. For users who want to maintain context across validators/walkers, they can set this to false. What do you think?
AndreasALoew: I think both proposals as described above by @prashanthjos and @stevehu are viable.

Which one is preferable IMO also depends on the scenario for keeping and reusing CollectorContexts throughout a series of multiple validations (which I admint I still haven't really understood). If it is possible that both approaches can be dynamically mixed and matched, then the additional parameter to validate() and walk() seems more appropriate, while in contrast in case it is a strict either-or whether you want to reuse CollectorContexts throughout more than one message or not, a config property whould be sufficient and even easier from a user perspective, as it might be a little more streamlined with the existing config properties...

So can you describe the use case for reusing a CollectorContext across multipe requests? This should help im making the best decision about which way to implement it.
AndreasALoew: @prashanthjos From everything that I can tell (without having run the reproducer myself to definitely confirm) it looks to me that your changes do properly reset thread local storage at the end of validate() and walk() and therefore, successfully prevent the memory leak.

Sorry, as I am pretty new to the internals of the validator, I cannot really comment on the details of the additional refactorings/code cleanups that you have done - they also look ok at my first glance...

Patch:
diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
index 5182bf5..897093b 100644
--- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
@@ -64,6 +64,18 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
         parseErrorCode(getValidatorType().getErrorCodeKey());
     }
 
+    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 Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         if (logger.isDebugEnabled()) debug(logger, node, rootNode, at);
 
@@ -73,6 +85,13 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
             return errors;
         }
 
+        // if allowAdditionalProperties is true, add all the properties as evaluated.
+        if (allowAdditionalProperties) {
+            for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
+                addToEvaluatedProperties(at + "." + it.next());
+            }
+        }
+
         for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
             String pname = it.next();
             // skip the context items
@@ -106,6 +125,47 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
         return Collections.unmodifiableSet(errors);
     }
 
+    @Override
+    public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
+        if (shouldValidateSchema) {
+            return validate(node, rootNode, at);
+        }
+
+        if (node == null || !node.isObject()) {
+            // ignore no object
+            return Collections.emptySet();
+        }
+
+        // Else continue walking.
+        for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
+            String pname = it.next();
+            // skip the context items
+            if (pname.startsWith("#")) {
+                continue;
+            }
+            boolean handledByPatternProperties = false;
+            for (Pattern pattern : patternProperties) {
+                Matcher m = pattern.matcher(pname);
+                if (m.find()) {
+                    handledByPatternProperties = true;
+                    break;
+                }
+            }
+
+            if (!allowedProperties.contains(pname) && !handledByPatternProperties) {
+                if (allowAdditionalProperties) {
+                    if (additionalPropertiesSchema != null) {
+                        ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
+                        if (state != null && state.isWalkEnabled()) {
+                           additionalPropertiesSchema.walk(node.get(pname), rootNode, at + "." + pname, state.isValidationEnabled());
+                        }
+                    }
+                }
+            }
+        }
+        return Collections.emptySet();
+    }
+
     @Override
     public void preloadJsonSchema() {
         if(additionalPropertiesSchema!=null) {
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index c84b56a..cd3c3df 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -16,12 +16,7 @@
 
 package com.networknt.schema;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
@@ -49,17 +44,35 @@ public class AllOfValidator 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>();
+        // get the Validator state object storing validation data
+        ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
+
+        Set<ValidationMessage> childSchemaErrors = 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<>());
+        List<String> totalEvaluatedProperties = new ArrayList<>();
 
         for (JsonSchema schema : schemas) {
             try {
-                errors.addAll(schema.validate(node, rootNode, at));
+                // Make the evaluatedProperties list empty.
+                CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, new ArrayList<>());
+
+                Set<ValidationMessage> localErrors = new HashSet<>();
+
+                if (!state.isWalkEnabled()) {
+                    localErrors = schema.validate(node, rootNode, at);
+                } else {
+                    localErrors = schema.walk(node, rootNode, at, true);
+                }
+
+                childSchemaErrors.addAll(localErrors);
+
+                // Keep Collecting total evaluated properties.
+                if (localErrors.isEmpty()) {
+                    totalEvaluatedProperties.addAll((List<String>) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES));
+                }
 
                 if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
                     final Iterator<JsonNode> arrayElements = schemaNode.elements();
@@ -93,9 +106,9 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
                     }
                 }
             } finally {
-                if (errors.isEmpty()) {
+                if (childSchemaErrors.isEmpty()) {
                     List<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
-                    backupEvaluatedPropertiesList.addAll((List<String>) CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES));
+                    backupEvaluatedPropertiesList.addAll(totalEvaluatedProperties);
                     CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedPropertiesList);
                 } else {
                     CollectorContext.getInstance().add(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES, backupEvaluatedProperties);
@@ -103,18 +116,19 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
             }
         }
 
-        return Collections.unmodifiableSet(errors);
+        return Collections.unmodifiableSet(childSchemaErrors);
     }
 
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
-        Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
-
+        if (shouldValidateSchema) {
+            return validate(node, rootNode, at);
+        }
         for (JsonSchema schema : schemas) {
             // Walk through the schema
-            validationMessages.addAll(schema.walk(node, rootNode, at, shouldValidateSchema));
+            schema.walk(node, rootNode, at, false);
         }
-        return Collections.unmodifiableSet(validationMessages);
+        return Collections.emptySet();
     }
 
     @Override
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index ae50409..66ebd5c 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -53,6 +53,9 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
+        // get the Validator state object storing validation data
+        ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
+
         if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
             validationContext.enterDiscriminatorContext(this.discriminatorContext, at);
         }
@@ -68,6 +71,7 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
 
         try {
             for (JsonSchema schema : schemas) {
+                Set<ValidationMessage> errors = new HashSet<>();
                 if (schema.getValidators().containsKey(typeValidatorName)) {
                     TypeValidator typeValidator = ((TypeValidator) schema.getValidators().get(typeValidatorName));
                     //If schema has type validator and node type doesn't match with schemaType then ignore it
@@ -77,7 +81,11 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
                         continue;
                     }
                 }
-                Set<ValidationMessage> errors = schema.validate(node, rootNode, at);
+                if (!state.isWalkEnabled()) {
+                    errors = schema.validate(node, rootNode, at);
+                } else {
+                    errors = schema.walk(node, rootNode, at, true);
+                }
                 if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) {
                     // Clear all errors.
                     allErrors.clear();
@@ -125,21 +133,13 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
 
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
-        ArrayList<Set<ValidationMessage>> results = new ArrayList<>(schemas.size());
-        for (JsonSchema schema : schemas) {
-            results.add(schema.walk(node, rootNode, at, shouldValidateSchema));
+        if (shouldValidateSchema) {
+            return validate(node, rootNode, at);
         }
-        if(! shouldValidateSchema) {
-            return new LinkedHashSet<>();
-        }
-        boolean atLeastOneValid = results.stream()
-                .anyMatch(Set::isEmpty);
-        if(atLeastOneValid) {
-            return new LinkedHashSet<>();
+        for (JsonSchema schema : schemas) {
+            schema.walk(node, rootNode, at, false);
         }
-        return results.stream()
-                .flatMap(Collection::stream)
-                .collect(Collectors.toCollection(LinkedHashSet::new));
+        return new LinkedHashSet<>();
     }
 
     @Override
diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java
index 0adb254..00254e7 100644
--- a/src/main/java/com/networknt/schema/IfValidator.java
+++ b/src/main/java/com/networknt/schema/IfValidator.java
@@ -71,7 +71,7 @@ public class IfValidator extends BaseJsonValidator implements JsonValidator {
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        boolean ifConditionPassed;
+        boolean ifConditionPassed = false;
         try {
             try {
                 ifConditionPassed = ifSchema.validate(node, rootNode, at).isEmpty();
@@ -108,7 +108,8 @@ public class IfValidator extends BaseJsonValidator implements JsonValidator {
             if (errors.isEmpty()) {
                 List<String> backupEvaluatedPropertiesList = (backupEvaluatedProperties == null ? new ArrayList<>() : (List<String>) backupEvaluatedProperties);
 
-                if (ifEvaluatedProperties != null) {
+                // If the "if" keyword condition is passed then only add if properties as evaluated.
+                if (ifEvaluatedProperties != null && ifConditionPassed) {
                     backupEvaluatedPropertiesList.addAll((List<String>) ifEvaluatedProperties);
                 }
 
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index ea29a8e..125c8c9 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -258,10 +258,14 @@ public class JsonSchema extends BaseJsonValidator {
 
     @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;
+        try {
+            Set<ValidationMessage> errors = validate(node, node, AT_ROOT);
+            return errors;
+        } finally {
+            if (validationContext.getConfig().isResetCollectorContext()) {
+                CollectorContext.getInstance().reset();
+            }
+        }
     }
 
     public Set<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, String at) {
@@ -274,6 +278,10 @@ public class JsonSchema extends BaseJsonValidator {
         for (JsonValidator v : getValidators().values()) {
             errors.addAll(v.validate(jsonNode, rootNode, at));
         }
+
+        // Process UnEvaluatedProperties after all the validators are called if there are no errors.
+        errors.addAll(processUnEvaluatedProperties(jsonNode, rootNode, at, true, true));
+
         if (null != config && config.isOpenAPI3StyleDiscriminators()) {
             ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
             if (null != discriminator) {
@@ -324,10 +332,10 @@ public class JsonSchema extends BaseJsonValidator {
             SchemaValidatorsConfig config = validationContext.getConfig();
             // Get the collector context from the thread local.
             CollectorContext collectorContext = getCollectorContext();
+            // Set the walkEnabled and isValidationEnabled flag in internal validator state.
+            setValidatorState(false, true);
             // 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.
@@ -337,7 +345,9 @@ public class JsonSchema extends BaseJsonValidator {
             ValidationResult validationResult = new ValidationResult(errors, collectorContext);
             return validationResult;
         } finally {
-            ThreadInfo.remove(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY);
+            if (validationContext.getConfig().isResetCollectorContext()) {
+                CollectorContext.getInstance().reset();
+            }
         }
     }
 
@@ -353,24 +363,30 @@ public class JsonSchema extends BaseJsonValidator {
      * @return result of ValidationResult
      */
     public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) {
-        // Get the config.
-        SchemaValidatorsConfig config = validationContext.getConfig();
-        // Get the collector context from the thread local.
-        CollectorContext collectorContext = getCollectorContext();
-        // Set the walkEnabled flag in internal validator state.
-        setValidatorState(true, shouldValidateSchema);
-        // Walk through the schema.
-        Set<ValidationMessage> errors = walk(node, node, AT_ROOT, shouldValidateSchema);
-        // 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.
-            collectorContext.loadCollectors();
+        try {
+            // Get the config.
+            SchemaValidatorsConfig config = validationContext.getConfig();
+            // Get the collector context from the thread local.
+            CollectorContext collectorContext = getCollectorContext();
+            // Set the walkEnabled flag in internal validator state.
+            setValidatorState(true, shouldValidateSchema);
+            // Walk through the schema.
+            Set<ValidationMessage> errors = walk(node, node, AT_ROOT, shouldValidateSchema);
+            // 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.
+                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;
+        } finally {
+            if (validationContext.getConfig().isResetCollectorContext()) {
+                CollectorContext.getInstance().reset();
+            }
         }
-        // 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;
     }
 
     @Override
@@ -408,6 +424,10 @@ public class JsonSchema extends BaseJsonValidator {
                         validationMessages);
             }
         }
+        if (shouldValidateSchema) {
+            // Process UnEvaluatedProperties after all the validators are called if there are no errors.
+            validationMessages.addAll(processUnEvaluatedProperties(node, rootNode, at, true, true));
+        }
         return validationMessages;
     }
 
diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java
index 9de8ed3..1847d85 100644
--- a/src/main/java/com/networknt/schema/NotValidator.java
+++ b/src/main/java/com/networknt/schema/NotValidator.java
@@ -20,9 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Set;
+import java.util.*;
 
 public class NotValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
@@ -37,17 +35,37 @@ public class NotValidator extends BaseJsonValidator implements JsonValidator {
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
-        debug(logger, node, rootNode, at);
+        Set<ValidationMessage> errors = new HashSet<>();
 
-        Set<ValidationMessage> errors = schema.validate(node, rootNode, at);
-        if (errors.isEmpty()) {
-            return Collections.singleton(buildValidationMessage(at, schema.toString()));
+        //As not will contain a schema 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 {
+            debug(logger, node, rootNode, at);
+            errors = schema.validate(node, rootNode, at);
+            if (errors.isEmpty()) {
+                return Collections.singleton(buildValidationMessage(at, schema.toString()));
+            }
+            return Collections.emptySet();
+        } 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);
+            }
         }
-        return Collections.emptySet();
     }
     
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
+        if (shouldValidateSchema) {
+            return validate(node, rootNode, at);
+        }
     	return schema.walk(node, rootNode, at, shouldValidateSchema);
     }
 
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index b0555ff..2419193 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -24,10 +24,7 @@ import org.slf4j.LoggerFactory;
 
 import java.net.URI;
 import java.text.MessageFormat;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.Set;
+import java.util.*;
 
 public class RefValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RefValidator.class);
@@ -130,29 +127,70 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
-        debug(logger, node, rootNode, at);
-        // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
-        // these schemas will be cached along with config. We have to replace the config for cached $ref references
-        // with the latest config. Reset the config.
-        schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig());
-        if (schema != null) {
-            return schema.validate(node, rootNode, at);
-        } else {
-            return Collections.emptySet();
+
+        Set<ValidationMessage> errors = new HashSet<>();
+
+        // As ref will contain a schema 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 {
+            debug(logger, node, rootNode, at);
+            // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
+            // these schemas will be cached along with config. We have to replace the config for cached $ref references
+            // with the latest config. Reset the config.
+            schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig());
+            if (schema != null) {
+                errors =  schema.validate(node, rootNode, at);
+            } else {
+                errors = Collections.emptySet();
+            }
+        } 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);
+            }
         }
+        return errors;
     }
 
-	@Override
-	public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
-        // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
-        // these schemas will be cached along with config. We have to replace the config for cached $ref references
-        // with the latest config. Reset the config.
-        schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig());
-		if (schema != null) {
-			return schema.walk(node, rootNode, at, shouldValidateSchema);
-		}
-		return Collections.emptySet();
-	}
+    @Override
+    public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
+
+        Set<ValidationMessage> errors = new HashSet<>();
+
+        // As ref will contain a schema 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 {
+            debug(logger, node, rootNode, at);
+            // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
+            // these schemas will be cached along with config. We have to replace the config for cached $ref references
+            // with the latest config. Reset the config.
+            schema.getSchema().getValidationContext().setConfig(parentSchema.getValidationContext().getConfig());
+            if (schema != null) {
+                errors = schema.walk(node, rootNode, at, shouldValidateSchema);
+            }
+            return errors;
+        } finally {
+            if (shouldValidateSchema) {
+                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);
+                }
+            }
+        }
+    }
 
 	public JsonSchemaRef getSchemaRef() {
 		return schema;
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index b822e61..e34dc91 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -80,6 +80,11 @@ public class SchemaValidatorsConfig {
      */
     private boolean handleNullableField = true;
 
+    /**
+     * When set to true resets the {@link CollectorContext} by calling {@link CollectorContext#reset()}.
+     */
+    private boolean resetCollectorContext = true;
+
     // This is just a constant for listening to all Keywords.
     public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener";
 
@@ -280,4 +285,12 @@ public class SchemaValidatorsConfig {
     public boolean doLoadCollectors() {
         return loadCollectors;
     }
+
+    public boolean isResetCollectorContext() {
+        return resetCollectorContext;
+    }
+
+    public void setResetCollectorContext(boolean resetCollectorContext) {
+        this.resetCollectorContext = resetCollectorContext;
+    }
 }
diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java
index f5b2485..c2f54b9 100644
--- a/src/main/java/com/networknt/schema/TypeValidator.java
+++ b/src/main/java/com/networknt/schema/TypeValidator.java
@@ -119,7 +119,8 @@ public class TypeValidator extends BaseJsonValidator implements JsonValidator {
             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")) {
+        // Hack to catch patternProperties like "^foo":"value"
+        if (schemaPath.endsWith("/type")) {
             addToEvaluatedProperties(at);
         }
         return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
index db6a6bc..791f24a 100644
--- a/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
@@ -17,6 +17,8 @@
 package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.BooleanNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -34,38 +36,51 @@ public class UnEvaluatedPropertiesValidator extends BaseJsonValidator implements
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
+
+        // Check if unevaluatedProperties is a boolean value.
         if (!schemaNode.isBoolean()) {
             return Collections.emptySet();
         }
-        // Check if unevaluatedProperties is false or true.
+
+        // Continue checking unevaluatedProperties.
         boolean unevaluatedProperties = schemaNode.booleanValue();
-        // Process all paths in the data node.
+
+        // Process all paths in 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.
+
+        // Check for errors only if unevaluatedProperties is false.
         if (!unevaluatedProperties) {
+
+            // Process UnEvaluated Properties.
+            List<String> unEvaluatedProperties = getUnEvaluatedProperties(allPaths);
+
+            // If unevaluatedProperties is not empty add error.
             if (!unEvaluatedProperties.isEmpty()) {
+                CollectorContext.getInstance().add(UNEVALUATED_PROPERTIES, unEvaluatedProperties);
                 return Collections.singleton(buildValidationMessage(String.join(", ", unEvaluatedProperties)));
             }
+        } else {
+            // Add all properties as evaluated.
+            CollectorContext.getInstance().add(EVALUATED_PROPERTIES, allPaths);
         }
         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;
+        List<String> unevaluatedPropertiesList = new ArrayList<>();
+        Object evaluatedPropertiesObj = CollectorContext.getInstance().get(UnEvaluatedPropertiesValidator.EVALUATED_PROPERTIES);
+        if (evaluatedPropertiesObj != null) {
+            List<String> evaluatedPropertiesList = (List<String>) evaluatedPropertiesObj;
             allPaths.forEach(path -> {
                 if (!evaluatedPropertiesList.contains(path)) {
-                    unevaluatedProperties.add(path);
+                    unevaluatedPropertiesList.add(path);
                 }
             });
+        } else {
+            unevaluatedPropertiesList.addAll(allPaths);
         }
-        return unevaluatedProperties;
+        return unevaluatedPropertiesList;
     }
 
     public void processAllPaths(JsonNode node, String at, List<String> paths) {
@@ -75,9 +90,8 @@ public class UnEvaluatedPropertiesValidator extends BaseJsonValidator implements
             JsonNode jsonNode = node.get(fieldName);
             if (jsonNode.isObject()) {
                 processAllPaths(jsonNode, at + "." + fieldName, paths);
-            } else {
-                paths.add(at + "." + fieldName);
             }
+            paths.add(at + "." + fieldName);
         }
     }
 
@@ -88,4 +102,4 @@ public class UnEvaluatedPropertiesValidator extends BaseJsonValidator implements
         }
         return Collections.emptySet();
     }
-}
+}
\ No newline at end of file


Test patch:
diff --git a/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java b/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java
index 5bed72c..2a316a4 100644
--- a/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java
+++ b/src/test/java/com/networknt/schema/BaseSuiteJsonSchemaTest.java
@@ -106,12 +106,14 @@ public abstract class BaseSuiteJsonSchemaTest {
                     } else {
                         if (errors.isEmpty()) {
                             System.out.println("---- test case failed ----");
+                            System.out.println("Description: " + test.get("description"));
                             System.out.println("schema: " + schema);
                             System.out.println("data: " + test.get("data"));
                         } else {
                             JsonNode errorCount = test.get("errorCount");
                             if (errorCount != null && errorCount.isInt() && errors.size() != errorCount.asInt()) {
                                 System.out.println("---- test case failed ----");
+                                System.out.println("Description: " + test.get("description"));
                                 System.out.println("schema: " + schema);
                                 System.out.println("data: " + test.get("data"));
                                 System.out.println("errors: " + errors);
@@ -125,6 +127,7 @@ public abstract class BaseSuiteJsonSchemaTest {
                     ArrayNode expectedValidationMesgs = (ArrayNode) test.get(VALIDATION_MESSAGES);
                     if (errors.isEmpty() && expectedValidationMesgs != null && expectedValidationMesgs.size() > 0) {
                         System.out.println("---- test case failed ----");
+                        System.out.println("Description: " + test.get("description"));
                         System.out.println("schema: " + schema);
                         System.out.println("data: " + test.get("data"));
                         System.out.println("Expected Validation Messages: " + expectedValidationMesgs);
@@ -143,6 +146,7 @@ public abstract class BaseSuiteJsonSchemaTest {
                             }
                             if (!found) {
                                 System.out.println("---- test case failed ----");
+                                System.out.println("Description: " + test.get("description"));
                                 System.out.println("schema: " + schema);
                                 System.out.println("data: " + test.get("data"));
                                 System.out.println("errors: " + errors);
diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java
index e67ca8d..be0e08c 100644
--- a/src/test/java/com/networknt/schema/CollectorContextTest.java
+++ b/src/test/java/com/networknt/schema/CollectorContextTest.java
@@ -150,8 +150,10 @@ public class CollectorContextTest {
         final JsonSchemaFactory schemaFactory = JsonSchemaFactory
                 .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema)
                 .build();
-        this.jsonSchema = schemaFactory.getSchema(getSchemaString());
-        this.jsonSchemaForCombine = schemaFactory.getSchema(getSchemaStringMultipleProperties());
+        SchemaValidatorsConfig schemaValidatorsConfig  = new SchemaValidatorsConfig();
+        schemaValidatorsConfig.setResetCollectorContext(false);
+        this.jsonSchema = schemaFactory.getSchema(getSchemaString(), schemaValidatorsConfig);
+        this.jsonSchemaForCombine = schemaFactory.getSchema(getSchemaStringMultipleProperties(), schemaValidatorsConfig);
     }
 
     private String getSchemaString() {
diff --git a/src/test/java/com/networknt/schema/Issue451Test.java b/src/test/java/com/networknt/schema/Issue451Test.java
index d7489fb..2fba7c6 100644
--- a/src/test/java/com/networknt/schema/Issue451Test.java
+++ b/src/test/java/com/networknt/schema/Issue451Test.java
@@ -25,6 +25,7 @@ public class Issue451Test {
     protected JsonSchema getJsonSchemaFromStreamContentV7(InputStream schemaContent) {
         JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
         SchemaValidatorsConfig svc = new SchemaValidatorsConfig();
+        svc.setResetCollectorContext(false);
         svc.addPropertyWalkListener(new CountingWalker());
         return factory.getSchema(schemaContent, svc);
     }
diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java
index 13b6d87..45025b3 100644
--- a/src/test/java/com/networknt/schema/JsonWalkTest.java
+++ b/src/test/java/com/networknt/schema/JsonWalkTest.java
@@ -47,6 +47,7 @@ public class JsonWalkTest {
         schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener());
         schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(),
                 new PropertiesKeywordListener());
+        schemaValidatorsConfig.setResetCollectorContext(false);
         final JsonSchemaFactory schemaFactory = JsonSchemaFactory
                 .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema)
                 .build();
@@ -56,6 +57,7 @@ public class JsonWalkTest {
         schemaValidatorsConfig1.addKeywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener());
         schemaValidatorsConfig1.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(),
                 new PropertiesKeywordListener());
+        schemaValidatorsConfig1.setResetCollectorContext(false);
         this.jsonSchema1 = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig1);
     }
 
diff --git a/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java b/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java
index cf49dd1..f29d0b1 100644
--- a/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java
+++ b/src/test/java/com/networknt/schema/UnevaluatedPropertiesTest.java
@@ -12,7 +12,7 @@ public class UnevaluatedPropertiesTest extends BaseSuiteJsonSchemaTest {
 
 
     @Test
-    public void testWalk() throws Exception {
+    public void testUnevaluatedProperties() 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
index 7ab1993..2519dcf 100644
--- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json
+++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json
@@ -120,7 +120,7 @@
         },
         "valid": false,
         "validationMessages": [
-          "There are unevaluated properties at following paths $.invalid1, $.address.invalid2"
+          "There are unevaluated properties at following paths $.address.invalid2"
         ]
       },
       {
@@ -538,7 +538,7 @@
           "age": 18,
           "surName": "Sur Name"
         },
-        "valid": true
+        "valid": false
       },
       {
         "description": "Data - else schema with one unevaluated property",
@@ -549,7 +549,7 @@
         },
         "valid": false,
         "validationMessages": [
-          "There are unevaluated properties at following paths $.unevaluated"
+          "There are unevaluated properties at following paths $.age, $.unevaluated"
         ]
       }
     ]
@@ -632,5 +632,1523 @@
         "valid": true
       }
     ]
+  },
+  {
+    "description": "unevaluatedProperties true",
+    "schema": {
+      "type": "object",
+      "unevaluatedProperties": true
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {},
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties false",
+    "schema": {
+      "type": "object",
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {},
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with adjacent properties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with adjacent patternProperties",
+    "schema": {
+      "type": "object",
+      "patternProperties": {
+        "^foo": {
+          "type": "string"
+        }
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with adjacent additionalProperties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "additionalProperties": true,
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no additional properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with nested properties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "properties": {
+            "bar": {
+              "type": "string"
+            }
+          }
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "with additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with nested patternProperties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "patternProperties": {
+            "^bar": {
+              "type": "string"
+            }
+          }
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "with additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with nested additionalProperties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "additionalProperties": true
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no additional properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with additional properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with nested unevaluatedProperties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "unevaluatedProperties": true
+        }
+      ],
+      "unevaluatedProperties": {
+        "type": "string",
+        "maxLength": 2
+      }
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with anyOf",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "anyOf": [
+        {
+          "properties": {
+            "bar": {
+              "const": "bar"
+            }
+          },
+          "required": [
+            "bar"
+          ]
+        },
+        {
+          "properties": {
+            "baz": {
+              "const": "baz"
+            }
+          },
+          "required": [
+            "baz"
+          ]
+        },
+        {
+          "properties": {
+            "quux": {
+              "const": "quux"
+            }
+          },
+          "required": [
+            "quux"
+          ]
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "when one matches and has no unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "when one matches and has unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "baz": "not-baz"
+        },
+        "valid": false
+      },
+      {
+        "description": "when two match and has unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "baz": "baz",
+          "quux": "not-quux"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with oneOf",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "oneOf": [
+        {
+          "properties": {
+            "bar": {
+              "const": "bar"
+            }
+          },
+          "required": [
+            "bar"
+          ]
+        },
+        {
+          "properties": {
+            "baz": {
+              "const": "baz"
+            }
+          },
+          "required": [
+            "baz"
+          ]
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "quux": "quux"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with not",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "not": {
+        "not": {
+          "properties": {
+            "bar": {
+              "const": "bar"
+            }
+          },
+          "required": [
+            "bar"
+          ]
+        }
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with if/then/else",
+    "schema": {
+      "type": "object",
+      "if": {
+        "properties": {
+          "foo": {
+            "const": "then"
+          }
+        },
+        "required": [
+          "foo"
+        ]
+      },
+      "then": {
+        "properties": {
+          "bar": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "bar"
+        ]
+      },
+      "else": {
+        "properties": {
+          "baz": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "baz"
+        ]
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "when if is true and has no unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "when if is true and has unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      },
+      {
+        "description": "when if is false and has no unevaluated properties",
+        "data": {
+          "baz": "baz"
+        },
+        "valid": true
+      },
+      {
+        "description": "when if is false and has unevaluated properties",
+        "data": {
+          "foo": "else",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with if/then/else, then not defined",
+    "schema": {
+      "type": "object",
+      "if": {
+        "properties": {
+          "foo": {
+            "const": "then"
+          }
+        },
+        "required": [
+          "foo"
+        ]
+      },
+      "else": {
+        "properties": {
+          "baz": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "baz"
+        ]
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "when if is true and has no unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar"
+        },
+        "valid": false
+      },
+      {
+        "description": "when if is true and has unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      },
+      {
+        "description": "when if is false and has no unevaluated properties",
+        "data": {
+          "baz": "baz"
+        },
+        "valid": true
+      },
+      {
+        "description": "when if is false and has unevaluated properties",
+        "data": {
+          "foo": "else",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with if/then/else, else not defined",
+    "schema": {
+      "type": "object",
+      "if": {
+        "properties": {
+          "foo": {
+            "const": "then"
+          }
+        },
+        "required": [
+          "foo"
+        ]
+      },
+      "then": {
+        "properties": {
+          "bar": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "bar"
+        ]
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "when if is true and has no unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "when if is true and has unevaluated properties",
+        "data": {
+          "foo": "then",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      },
+      {
+        "description": "when if is false and has no unevaluated properties",
+        "data": {
+          "baz": "baz"
+        },
+        "valid": false
+      },
+      {
+        "description": "when if is false and has unevaluated properties",
+        "data": {
+          "foo": "else",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with dependentSchemas",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "dependentSchemas": {
+        "foo": {
+          "properties": {
+            "bar": {
+              "const": "bar"
+            }
+          },
+          "required": [
+            "bar"
+          ]
+        }
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with boolean schemas",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        true
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties with $ref",
+    "schema": {
+      "type": "object",
+      "$ref": "#/$defs/bar",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "unevaluatedProperties": false,
+      "$defs": {
+        "bar": {
+          "properties": {
+            "bar": {
+              "type": "string"
+            }
+          }
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "with no unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      },
+      {
+        "description": "with unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar",
+          "baz": "baz"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties can't see inside cousins",
+    "schema": {
+      "allOf": [
+        {
+          "properties": {
+            "foo": true
+          }
+        },
+        {
+          "unevaluatedProperties": false
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "always fails",
+        "data": {
+          "foo": 1
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "nested unevaluatedProperties, outer false, inner true, properties outside",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "unevaluatedProperties": true
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "nested unevaluatedProperties, outer false, inner true, properties inside",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "properties": {
+            "foo": {
+              "type": "string"
+            }
+          },
+          "unevaluatedProperties": true
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "nested unevaluatedProperties, outer true, inner false, properties outside",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "string"
+        }
+      },
+      "allOf": [
+        {
+          "unevaluatedProperties": false
+        }
+      ],
+      "unevaluatedProperties": true
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": false
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "nested unevaluatedProperties, outer true, inner false, properties inside",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "properties": {
+            "foo": {
+              "type": "string"
+            }
+          },
+          "unevaluatedProperties": false
+        }
+      ],
+      "unevaluatedProperties": true
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "cousin unevaluatedProperties, true and false, true with properties",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "properties": {
+            "foo": {
+              "type": "string"
+            }
+          },
+          "unevaluatedProperties": true
+        },
+        {
+          "unevaluatedProperties": false
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": false
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "cousin unevaluatedProperties, true and false, false with properties",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "unevaluatedProperties": true
+        },
+        {
+          "properties": {
+            "foo": {
+              "type": "string"
+            }
+          },
+          "unevaluatedProperties": false
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "with no nested unevaluated properties",
+        "data": {
+          "foo": "foo"
+        },
+        "valid": true
+      },
+      {
+        "description": "with nested unevaluated properties",
+        "data": {
+          "foo": "foo",
+          "bar": "bar"
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "property is evaluated in an uncle schema to unevaluatedProperties",
+    "comment": "see https://stackoverflow.com/questions/66936884/deeply-nested-unevaluatedproperties-and-their-expectations",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "foo": {
+          "type": "object",
+          "properties": {
+            "bar": {
+              "type": "string"
+            }
+          },
+          "unevaluatedProperties": false
+        }
+      },
+      "anyOf": [
+        {
+          "properties": {
+            "foo": {
+              "properties": {
+                "faz": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "no extra properties",
+        "data": {
+          "foo": {
+            "bar": "test"
+          }
+        },
+        "valid": true
+      },
+      {
+        "description": "uncle keyword evaluation is not significant",
+        "data": {
+          "foo": {
+            "bar": "test",
+            "faz": "test"
+          }
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "in-place applicator siblings, allOf has unevaluated",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "properties": {
+            "foo": true
+          },
+          "unevaluatedProperties": false
+        }
+      ],
+      "anyOf": [
+        {
+          "properties": {
+            "bar": true
+          }
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "base case: both properties present",
+        "data": {
+          "foo": 1,
+          "bar": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "in place applicator siblings, bar is missing",
+        "data": {
+          "foo": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "in place applicator siblings, foo is missing",
+        "data": {
+          "bar": 1
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "in-place applicator siblings, anyOf has unevaluated",
+    "schema": {
+      "type": "object",
+      "allOf": [
+        {
+          "properties": {
+            "foo": true
+          }
+        }
+      ],
+      "anyOf": [
+        {
+          "properties": {
+            "bar": true
+          },
+          "unevaluatedProperties": false
+        }
+      ]
+    },
+    "tests": [
+      {
+        "description": "base case: both properties present",
+        "data": {
+          "foo": 1,
+          "bar": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "in place applicator siblings, bar is missing",
+        "data": {
+          "foo": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "in place applicator siblings, foo is missing",
+        "data": {
+          "bar": 1
+        },
+        "valid": true
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties + single cyclic ref",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "x": {
+          "$ref": "#"
+        }
+      },
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "Empty is valid",
+        "data": {},
+        "valid": true
+      },
+      {
+        "description": "Single is valid",
+        "data": {
+          "x": {}
+        },
+        "valid": true
+      },
+      {
+        "description": "Unevaluated on 1st level is invalid",
+        "data": {
+          "x": {},
+          "y": {}
+        },
+        "valid": false
+      },
+      {
+        "description": "Nested is valid",
+        "data": {
+          "x": {
+            "x": {}
+          }
+        },
+        "valid": true
+      },
+      {
+        "description": "Unevaluated on 2nd level is invalid",
+        "data": {
+          "x": {
+            "x": {},
+            "y": {}
+          }
+        },
+        "valid": false
+      },
+      {
+        "description": "Deep nested is valid",
+        "data": {
+          "x": {
+            "x": {
+              "x": {}
+            }
+          }
+        },
+        "valid": true
+      },
+      {
+        "description": "Unevaluated on 3rd level is invalid",
+        "data": {
+          "x": {
+            "x": {
+              "x": {},
+              "y": {}
+            }
+          }
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "unevaluatedProperties + ref inside allOf / oneOf",
+    "schema": {
+      "$defs": {
+        "one": {
+          "properties": {
+            "a": true
+          }
+        },
+        "two": {
+          "required": [
+            "x"
+          ],
+          "properties": {
+            "x": true
+          }
+        }
+      },
+      "allOf": [
+        {
+          "$ref": "#/$defs/one"
+        },
+        {
+          "properties": {
+            "b": true
+          }
+        },
+        {
+          "oneOf": [
+            {
+              "$ref": "#/$defs/two"
+            },
+            {
+              "required": [
+                "y"
+              ],
+              "properties": {
+                "y": true
+              }
+            }
+          ]
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "Empty is invalid (no x or y)",
+        "data": {},
+        "valid": false
+      },
+      {
+        "description": "a and b are invalid (no x or y)",
+        "data": {
+          "a": 1,
+          "b": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "x and y are invalid",
+        "data": {
+          "x": 1,
+          "y": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "a and x are valid",
+        "data": {
+          "a": 1,
+          "x": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "a and y are valid",
+        "data": {
+          "a": 1,
+          "y": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "a and b and x are valid",
+        "data": {
+          "a": 1,
+          "b": 1,
+          "x": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "a and b and y are valid",
+        "data": {
+          "a": 1,
+          "b": 1,
+          "y": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "a and b and x and y are invalid",
+        "data": {
+          "a": 1,
+          "b": 1,
+          "x": 1,
+          "y": 1
+        },
+        "valid": false
+      }
+    ]
+  },
+  {
+    "description": "dynamic evalation inside nested refs",
+    "schema": {
+      "$defs": {
+        "one": {
+          "oneOf": [
+            {
+              "$ref": "#/$defs/two"
+            },
+            {
+              "required": [
+                "b"
+              ],
+              "properties": {
+                "b": true
+              }
+            },
+            {
+              "required": [
+                "xx"
+              ],
+              "patternProperties": {
+                "x": true
+              }
+            },
+            {
+              "required": [
+                "all"
+              ],
+              "unevaluatedProperties": true
+            }
+          ]
+        },
+        "two": {
+          "oneOf": [
+            {
+              "required": [
+                "c"
+              ],
+              "properties": {
+                "c": true
+              }
+            },
+            {
+              "required": [
+                "d"
+              ],
+              "properties": {
+                "d": true
+              }
+            }
+          ]
+        }
+      },
+      "oneOf": [
+        {
+          "$ref": "#/$defs/one"
+        },
+        {
+          "required": [
+            "a"
+          ],
+          "properties": {
+            "a": true
+          }
+        }
+      ],
+      "unevaluatedProperties": false
+    },
+    "tests": [
+      {
+        "description": "Empty is invalid",
+        "data": {},
+        "valid": false
+      },
+      {
+        "description": "a is valid",
+        "data": {
+          "a": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "b is valid",
+        "data": {
+          "b": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "c is valid",
+        "data": {
+          "c": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "d is valid",
+        "data": {
+          "d": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "a + b is invalid",
+        "data": {
+          "a": 1,
+          "b": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "a + c is invalid",
+        "data": {
+          "a": 1,
+          "c": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "a + d is invalid",
+        "data": {
+          "a": 1,
+          "d": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "b + c is invalid",
+        "data": {
+          "b": 1,
+          "c": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "b + d is invalid",
+        "data": {
+          "b": 1,
+          "d": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "c + d is invalid",
+        "data": {
+          "c": 1,
+          "d": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "xx + a is invalid",
+        "data": {
+          "xx": 1,
+          "a": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "xx + b is invalid",
+        "data": {
+          "xx": 1,
+          "b": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "xx + c is invalid",
+        "data": {
+          "xx": 1,
+          "c": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "xx + d is invalid",
+        "data": {
+          "xx": 1,
+          "d": 1
+        },
+        "valid": false
+      },
+      {
+        "description": "all is valid",
+        "data": {
+          "all": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "all + foo is valid",
+        "data": {
+          "all": 1,
+          "foo": 1
+        },
+        "valid": true
+      },
+      {
+        "description": "all + a is invalid",
+        "data": {
+          "all": 1,
+          "a": 1
+        },
+        "valid": false
+      }
+    ]
   }
 ]
\ No newline at end of file