Repository: networknt/json-schema-validator (java)

Issue:
Misleading validation error for `anyOf` property when required value is invalid
### Summary

Hey everyone, we try to upgrade from version 1.0.69 to 1.0.77 and observe that the validation errors for a certain scenario changed starting with 1.0.74. The error is a bit misleading and I'd like to get your thoughts on whether the previous behavior can be restored or if it will stay this way. The detailed test case is attached. 

To sum it up, we have an object that can either be null or have a required enum property. If the property supplied is invalid, then:
* before 1.0.74, we would only get a validation error stating that `$.optionalObject.value: does not have a value in the enumeration`
* after 1.0.74, we would also get a message that `$.optionalObject: object found, null expected` which is not true, since `optionalObject` can be an object, it just happens that this object is not valid.

I'd expect to only get a message about the value in this case.

### Reproducer

```
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidationMessage;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ReproducerTest {
    private static final String SCHEMA = """
        {
           "$schema": "http://json-schema.org/draft-07/schema#",
           "properties": {
               "optionalObject": {
                   "anyOf": [
                       {
                           "type": "null"
                       },
                       {
                           "type": "object",
                           "properties": {
                               "value": {
                                   "enum": [
                                       "one",
                                       "two"
                                   ]
                               }
                           },
                           "required": [
                               "value"
                           ]
                       }
                   ]
               }
           }
        }""";

    private static final String INVALID_VALUE = """
        {
            "optionalObject": {
                "value": "invalid"
            }
        }""";

    @Test
    void testCorrectErrorForInvalidValue() throws JsonProcessingException {
        var mapper = new ObjectMapper();
        var schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7).getSchema(SCHEMA);

        var errors = schema.validate(mapper.readTree(INVALID_VALUE));
        var errorMessages = errors.stream().map(ValidationMessage::getMessage).toList();

        assertTrue(errorMessages.contains("$.optionalObject.value: does not have a value in the enumeration [one, two]"));
        assertFalse(errorMessages.contains("$.optionalObject: object found, null expected"));
    }
}

```

Pull request:
Use full schema path to look up type validators for `anyOf` operator
### Summary
https://github.com/networknt/json-schema-validator/pull/626 changed how the schema path is set for `JsonSchema`s of `anyOf` declaration. However, it did not account for the fact that this schema path is used to identify type validators that belong to the operator. `AnyOfValidator#validate` still relied on the previous schema path and could not apply special handling to those validators (i.e. execute just the validator that matches the value). As a result, `TypeValidator`s for all options of `anyOf` property were evaluated separately and results aggregated. 

_Note_: I have no clue why the diff for `AnyOfValidator` is so ugly. The screenshot shows what exactly changed:
<img width="1631" alt="Screenshot 2023-02-16 at 2 14 59 PM" src="https://user-images.githubusercontent.com/14233799/219374766-c25e252e-d538-4693-883c-c1c5fad90de6.png">

Resolves #662 

Patch:
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index f7e719b..c0cf056 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -1,173 +1,177 @@
-/*
- * 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.*;
-import java.util.stream.Collectors;
-
-public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
-    private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
-    private static final String REMARK = "Remaining validation messages report why candidate schemas didn't match";
-    private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";
-
-    private final List<JsonSchema> schemas = new ArrayList<JsonSchema>();
-    private final ValidationContext.DiscriminatorContext discriminatorContext;
-
-    public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
-        super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
-        this.validationContext = validationContext;
-        int size = schemaNode.size();
-        for (int i = 0; i < size; i++) {
-            schemas.add(new JsonSchema(validationContext,
-                    parentSchema.getSchemaPath() + "/" + getValidatorType().getValue() + "/" + i,
-                    parentSchema.getCurrentUri(),
-                    schemaNode.get(i),
-                    parentSchema));
-        }
-
-        if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
-            this.discriminatorContext = new ValidationContext.DiscriminatorContext();
-        } else {
-            this.discriminatorContext = null;
-        }
-    }
-
-    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);
-        }
-
-        boolean initialHasMatchedNode = state.hasMatchedNode();
-
-        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 {
-            int numberOfValidSubSchemas = 0;
-            for (JsonSchema schema : schemas) {
-                state.setMatchedNode(initialHasMatchedNode);
-                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
-                    //For union type, it is a must to call TypeValidator
-                    if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
-                        allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
-                        continue;
-                    }
-                }
-                if (!state.isWalkEnabled()) {
-                    errors = schema.validate(node, rootNode, at);
-                } else {
-                    errors = schema.walk(node, rootNode, at, true);
-                }
-
-                // check if any validation errors have occurred
-                if (errors.isEmpty()) {
-                    // check whether there are no errors HOWEVER we have validated the exact validator
-                    if (!state.hasMatchedNode()) {
-                        continue;
-                    }
-                    // we found a valid subschema, so increase counter
-                    numberOfValidSubSchemas++;
-                }
-
-                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;
-                    }
-                }
-                allErrors.addAll(errors);
-            }
-
-            // determine only those errors which are NOT of type "required" property missing
-            Set<ValidationMessage> childNotRequiredErrors = allErrors.stream().filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())).collect(Collectors.toSet());
-
-            // in case we had at least one (anyOf, i.e. any number >= 1 of) valid subschemas, we can remove all other errors about "required" properties
-            if (numberOfValidSubSchemas >= 1 && childNotRequiredErrors.isEmpty()) {
-                allErrors = childNotRequiredErrors;
-            }
-
-            if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && discriminatorContext.isActive()) {
-                final Set<ValidationMessage> errors = new HashSet<ValidationMessage>();
-                errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
-                return Collections.unmodifiableSet(errors);
-            }
-        } finally {
-            if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
-                validationContext.leaveDiscriminatorContextImmediately(at);
-            }
-            if (allErrors.isEmpty()) {
-                addEvaluatedProperties(backupEvaluatedProperties);
-                state.setMatchedNode(true);
-            } 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) {
-        if (shouldValidateSchema) {
-            return validate(node, rootNode, at);
-        }
-        for (JsonSchema schema : schemas) {
-            schema.walk(node, rootNode, at, false);
-        }
-        return new LinkedHashSet<>();
-    }
-
-    @Override
-    public void preloadJsonSchema() {
-        preloadJsonSchemas(schemas);
-    }
-}
+/*
+ * 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.*;
+import java.util.stream.Collectors;
+
+public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
+    private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
+    private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";
+
+    private final List<JsonSchema> schemas = new ArrayList<>();
+    private final ValidationContext.DiscriminatorContext discriminatorContext;
+
+    public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
+        super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
+        this.validationContext = validationContext;
+        int size = schemaNode.size();
+        for (int i = 0; i < size; i++) {
+            schemas.add(new JsonSchema(validationContext,
+                getChildSchemaPath(i),
+                parentSchema.getCurrentUri(),
+                schemaNode.get(i),
+                parentSchema));
+        }
+
+        if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
+            this.discriminatorContext = new ValidationContext.DiscriminatorContext();
+        } else {
+            this.discriminatorContext = null;
+        }
+    }
+
+    private String getChildSchemaPath(int childIdx) {
+        return parentSchema.getSchemaPath() + "/" + getValidatorType().getValue() + "/" + childIdx;
+    }
+
+    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);
+        }
+
+        boolean initialHasMatchedNode = state.hasMatchedNode();
+
+        Set<ValidationMessage> allErrors = new LinkedHashSet<>();
+
+        // 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 {
+            int numberOfValidSubSchemas = 0;
+            for (int i = 0; i < schemas.size(); ++i) {
+                JsonSchema schema = schemas.get(i);
+                state.setMatchedNode(initialHasMatchedNode);
+                Set<ValidationMessage> errors;
+                String typeValidatorName = getChildSchemaPath(i) + "/type";
+                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
+                    //For union type, it is a must to call TypeValidator
+                    if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
+                        allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
+                        continue;
+                    }
+                }
+                if (!state.isWalkEnabled()) {
+                    errors = schema.validate(node, rootNode, at);
+                } else {
+                    errors = schema.walk(node, rootNode, at, true);
+                }
+
+                // check if any validation errors have occurred
+                if (errors.isEmpty()) {
+                    // check whether there are no errors HOWEVER we have validated the exact validator
+                    if (!state.hasMatchedNode()) {
+                        continue;
+                    }
+                    // we found a valid subschema, so increase counter
+                    numberOfValidSubSchemas++;
+                }
+
+                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;
+                    }
+                }
+                allErrors.addAll(errors);
+            }
+
+            // determine only those errors which are NOT of type "required" property missing
+            Set<ValidationMessage> childNotRequiredErrors = allErrors.stream().filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())).collect(Collectors.toSet());
+
+            // in case we had at least one (anyOf, i.e. any number >= 1 of) valid subschemas, we can remove all other errors about "required" properties
+            if (numberOfValidSubSchemas >= 1 && childNotRequiredErrors.isEmpty()) {
+                allErrors = childNotRequiredErrors;
+            }
+
+            if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && discriminatorContext.isActive()) {
+                final Set<ValidationMessage> errors = new HashSet<ValidationMessage>();
+                errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
+                return Collections.unmodifiableSet(errors);
+            }
+        } finally {
+            if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) {
+                validationContext.leaveDiscriminatorContextImmediately(at);
+            }
+            if (allErrors.isEmpty()) {
+                addEvaluatedProperties(backupEvaluatedProperties);
+                state.setMatchedNode(true);
+            } 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) {
+        if (shouldValidateSchema) {
+            return validate(node, rootNode, at);
+        }
+        for (JsonSchema schema : schemas) {
+            schema.walk(node, rootNode, at, false);
+        }
+        return new LinkedHashSet<>();
+    }
+
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemas);
+    }
+}


Test patch:
diff --git a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java
index 544a71f..4c4d63b 100644
--- a/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java
+++ b/src/test/java/com/networknt/schema/BaseJsonSchemaValidatorTest.java
@@ -30,11 +30,11 @@ import java.net.URL;
  */
 public class BaseJsonSchemaValidatorTest {
 
-    private ObjectMapper mapper = new ObjectMapper();
+    private static final ObjectMapper mapper = new ObjectMapper();
 
     protected JsonNode getJsonNodeFromClasspath(String name) throws IOException {
         InputStream is1 = Thread.currentThread().getContextClassLoader()
-                .getResourceAsStream(name);
+            .getResourceAsStream(name);
         return mapper.readTree(is1);
     }
 
@@ -46,10 +46,14 @@ public class BaseJsonSchemaValidatorTest {
         return mapper.readTree(new URL(url));
     }
 
-    protected JsonSchema getJsonSchemaFromClasspath(String name) {
-        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4);
+    protected static JsonSchema getJsonSchemaFromClasspath(String name) {
+        return getJsonSchemaFromClasspath(name, SpecVersion.VersionFlag.V4);
+    }
+
+    protected static JsonSchema getJsonSchemaFromClasspath(String name, SpecVersion.VersionFlag schemaVersion) {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(schemaVersion);
         InputStream is = Thread.currentThread().getContextClassLoader()
-                .getResourceAsStream(name);
+            .getResourceAsStream(name);
         return factory.getSchema(is);
     }
 
diff --git a/src/test/java/com/networknt/schema/Issue627Test.java b/src/test/java/com/networknt/schema/Issue627Test.java
deleted file mode 100644
index f51b55b..0000000
--- a/src/test/java/com/networknt/schema/Issue627Test.java
+++ /dev/null
@@ -1,2 +0,0 @@
-package com.networknt.schema;public class Issue627Test {
-}
diff --git a/src/test/java/com/networknt/schema/Issue662Test.java b/src/test/java/com/networknt/schema/Issue662Test.java
new file mode 100644
index 0000000..1535f0a
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue662Test.java
@@ -0,0 +1,60 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+import static java.util.stream.Collectors.toList;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Issue662Test extends BaseJsonSchemaValidatorTest {
+
+    private static final String RESOURCE_PREFIX = "issues/662/";
+    private static JsonSchema schema;
+
+    @BeforeAll
+    static void setup() {
+        schema = getJsonSchemaFromClasspath(resource("schema.json"), SpecVersion.VersionFlag.V7);
+    }
+
+    @Test
+    void testNoErrorsForEmptyObject() throws IOException {
+        JsonNode node = getJsonNodeFromClasspath(resource("emptyObject.json"));
+        Set<ValidationMessage> errors = schema.validate(node);
+        assertTrue(errors.isEmpty(), "No validation errors for empty optional object");
+    }
+
+    @Test
+    void testNoErrorsForValidObject() throws IOException {
+        JsonNode node = getJsonNodeFromClasspath(resource("validObject.json"));
+        Set<ValidationMessage> errors = schema.validate(node);
+        assertTrue(errors.isEmpty(), "No validation errors for a valid optional object");
+    }
+
+    @Test
+    void testCorrectErrorForInvalidValue() throws IOException {
+        JsonNode node = getJsonNodeFromClasspath(resource("objectInvalidValue.json"));
+        Set<ValidationMessage> errors = schema.validate(node);
+        List<String> errorMessages = errors.stream()
+            .map(ValidationMessage::getMessage)
+            .collect(toList());
+
+        assertTrue(
+            errorMessages.contains("$.optionalObject.value: does not have a value in the enumeration [one, two]"),
+            "Validation error for invalid object property is captured"
+        );
+        assertFalse(
+            errorMessages.contains("$.optionalObject: object found, null expected"),
+            "No validation error that the object is not expected"
+        );
+    }
+
+    private static String resource(String name) {
+        return RESOURCE_PREFIX + name;
+    }
+}
diff --git a/src/test/resources/issues/662/emptyObject.json b/src/test/resources/issues/662/emptyObject.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/src/test/resources/issues/662/emptyObject.json
@@ -0,0 +1,1 @@
+{}
diff --git a/src/test/resources/issues/662/objectInvalidValue.json b/src/test/resources/issues/662/objectInvalidValue.json
new file mode 100644
index 0000000..2245430
--- /dev/null
+++ b/src/test/resources/issues/662/objectInvalidValue.json
@@ -0,0 +1,5 @@
+{
+    "optionalObject": {
+        "value": "invalid"
+    }
+}
diff --git a/src/test/resources/issues/662/schema.json b/src/test/resources/issues/662/schema.json
new file mode 100644
index 0000000..3155e0f
--- /dev/null
+++ b/src/test/resources/issues/662/schema.json
@@ -0,0 +1,26 @@
+{
+    "$schema": "http://json-schema.org/draft-07/schema#",
+    "properties": {
+        "optionalObject": {
+            "anyOf": [
+                {
+                    "type": "null"
+                },
+                {
+                    "type": "object",
+                    "properties": {
+                        "value": {
+                            "enum": [
+                                "one",
+                                "two"
+                            ]
+                        }
+                    },
+                    "required": [
+                        "value"
+                    ]
+                }
+            ]
+        }
+    }
+}
diff --git a/src/test/resources/issues/662/validObject.json b/src/test/resources/issues/662/validObject.json
new file mode 100644
index 0000000..ce72d61
--- /dev/null
+++ b/src/test/resources/issues/662/validObject.json
@@ -0,0 +1,5 @@
+{
+    "optionalObject": {
+        "value": "one"
+    }
+}