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

Issue:
`unevaluatedProperties` not evaluating properties stemming from `$ref` schemas loaded through nested `allOf` 
I believe I found a bug when determining `unevaluatedProperties` (version 1.5.2). For now, I'll show where I run into issues in context of my actual codebase. My hope is this can already convey expectations vs. actual result. I can try creating a small repro later.

Take the follow schema:

```
{
  "$schema": "https://json-schema.org/draft/2019-09/schema",
  "type": "object",
  "allOf": [ { "$ref": "PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration" } ],
  "properties": {
    "__type": { "const": "dk.cachet.carp.common.application.devices.Smartphone" }
  },
  "unevaluatedProperties": false,
  "$defs": {
    "DeviceRegistration": {
      "$anchor": "DeviceRegistration",
      "$ref": "DefaultDeviceRegistration.json"
    }
  }
}
```

`PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration` in turn has a nested `allOf`:

```
    "PrimaryDeviceConfiguration": {
      "$anchor": "PrimaryDeviceConfiguration",
      "allOf": [ { "$ref": "DeviceConfiguration.json#DeviceConfiguration" } ],
      "properties": {
        "isPrimaryDevice": { "const": true }
      },
      "required": [ "isPrimaryDevice" ]
    }
  }
```

`DeviceConfiguration.json#DeviceConfiguration` in turn defines a `roleName` property.

The base schema referenced (`PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration`) doesn't set `unevaluatedProperties` anywhere. The intent is for the base types to allow extension, but _this specific type_, defined in this schema, shouldn't have any additional unknown properties.

However, with this setup, I get warnings on properties which are defined through my `allOf` schema.

> 'roleName' is not evaluated and the schema does not allow unevaluated properties

When I add this property directly to `allOf` in the first schema, the error is gone.

```
  "allOf": [
    { "$ref": "PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration" },
    {
      "properties": {
        "roleName": { "type": "string" }
      }
    }
  ]
```

When I reference a definition within the same schema, the error is also gone.

```
  "allOf": [
    { "$ref": "PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration" },
    { "$ref": "#/$defs/Test" }
  ]
  ...
  "$defs": {
    "Test": {
      "$anchor": "Test",
      "properties": {
        "roleName": { "type": "string" }
      }
    }
  }
```

When I add this property directly to the second schema (`PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration`), the error is also gone.

```
    "PrimaryDeviceConfiguration": {
      "$anchor": "PrimaryDeviceConfiguration",
      "allOf": [ { "$ref": "DeviceConfiguration.json#DeviceConfiguration" } ],
      "properties": {
        "isPrimaryDevice": { "const": true },
        "roleName": { "type": "string" }
      },
      "required": [ "isPrimaryDevice" ]
    }
  }
```

But, seemingly the property (`roleName`) being defined in the lowest schema (`DeviceConfiguration.json#DeviceConfiguration`) doesn't get picked up.

I would expect the JSON schema specification to dictate these also need to be included, but seemingly this library only recurses one level deep.

Pull request:
Fix hasAdjacentKeywordInEvaluationPath optimisation
Closes #1123

This fixes the incorrect logic for determining if there is a change in the instance location.

Patch:
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index b2775be..be816be 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -392,29 +392,18 @@ public abstract class BaseJsonValidator extends ValidationMessageHandler impleme
      */
     protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) {
         JsonSchema schema = getEvaluationParentSchema();
-        boolean checkInstance = false;
-        boolean anchor = false;
-        boolean stop = false;
         while (schema != null) {
             for (JsonValidator validator : schema.getValidators()) {
                 if (keyword.equals(validator.getKeyword())) {
                     return true;
                 }
-                if (checkInstance) {
-                    if ("properties".equals(validator.getKeyword()) || "items".equals(validator.getKeyword())) {
-                        stop = true;
-                    } else if ("$dynamicAnchor".equals(validator.getKeyword()) || "$recursiveAnchor".equals(validator.getKeyword())) {
-                        anchor = true;
-                    }
-                }
             }
-            if (stop && !anchor) {
+            Object element = schema.getEvaluationPath().getElement(-1);
+            if ("properties".equals(element) || "items".equals(element)) {
                 // If there is a change in instance location then return false
                 return false;
             }
             schema = schema.getEvaluationParentSchema();
-            checkInstance = true;
-            anchor = false;
         }
         return false;
     }


Test patch:
diff --git a/src/test/java/com/networknt/schema/UnevaluatedPropertiesValidatorTest.java b/src/test/java/com/networknt/schema/UnevaluatedPropertiesValidatorTest.java
index 447888d..2a8f7c4 100644
--- a/src/test/java/com/networknt/schema/UnevaluatedPropertiesValidatorTest.java
+++ b/src/test/java/com/networknt/schema/UnevaluatedPropertiesValidatorTest.java
@@ -17,14 +17,18 @@
 package com.networknt.schema;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
 import org.junit.jupiter.api.Test;
 
 import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.output.OutputUnit;
 
 /**
  * UnevaluatedPropertiesValidatorTest.
@@ -148,4 +152,92 @@ public class UnevaluatedPropertiesValidatorTest {
         assertEquals("type", assertions.get(0).getType());
         assertEquals("$.unevaluatedProperties.type", assertions.get(0).getEvaluationPath().toString());
     }
+
+    @Test
+    void ref() {
+        String schemaData = "{\r\n"
+                + "    \"definitions\": {\r\n"
+                + "        \"other\": {\r\n"
+                + "            \"type\": \"object\",\r\n"
+                + "            \"properties\": {\r\n"
+                + "                \"surfboard\": {\r\n"
+                + "                    \"type\": \"string\"\r\n"
+                + "                }\r\n"
+                + "            }\r\n"
+                + "        }\r\n"
+                + "    },\r\n"
+                + "    \"allOf\": [\r\n"
+                + "        {\r\n"
+                + "            \"$ref\": \"#/definitions/other\"\r\n"
+                + "        },\r\n"
+                + "        {\r\n"
+                + "            \"properties\": {\r\n"
+                + "                \"wheels\": {},\r\n"
+                + "                \"headlights\": {}\r\n"
+                + "            }\r\n"
+                + "        },\r\n"
+                + "        {\r\n"
+                + "            \"properties\": {\r\n"
+                + "                \"pontoons\": {}\r\n"
+                + "            }\r\n"
+                + "        },\r\n"
+                + "        {\r\n"
+                + "            \"properties\": {\r\n"
+                + "                \"wings\": {}\r\n"
+                + "            }\r\n"
+                + "        }\r\n"
+                + "    ],\r\n"
+                + "    \"unevaluatedProperties\": false\r\n"
+                + "}";
+        String inputData = "{ \"pontoons\": {}, \"wheels\": {}, \"surfboard\": \"2\" }";
+        JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V201909).getSchema(schemaData);
+        Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
+        assertEquals(0, messages.size());
+    }
+
+    @Test
+    void nestedRef() {
+        String schemaData = "{\r\n"
+                + "  \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\r\n"
+                + "  \"type\": \"object\",\r\n"
+                + "  \"allOf\": [ { \"$ref\": \"https://www.example.org/PrimaryDeviceConfiguration.json#PrimaryDeviceConfiguration\" } ],\r\n"
+                + "  \"properties\": {\r\n"
+                + "    \"__type\": { \"const\": \"dk.cachet.carp.common.application.devices.Smartphone\" }\r\n"
+                + "  },\r\n"
+                + "  \"unevaluatedProperties\": false\r\n"
+                + "}";
+        String primaryDeviceConfiguration =  "{\r\n"
+                + "  \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\r\n"
+                + "    \"PrimaryDeviceConfiguration\": {\r\n"
+                + "      \"$anchor\": \"PrimaryDeviceConfiguration\",\r\n"
+                + "      \"allOf\": [ { \"$ref\": \"DeviceConfiguration.json#DeviceConfiguration\" } ],\r\n"
+                + "      \"properties\": {\r\n"
+                + "        \"isPrimaryDevice\": { \"const\": true }\r\n"
+                + "      },\r\n"
+                + "      \"required\": [ \"isPrimaryDevice\" ]\r\n"
+                + "    }\r\n"
+                + "  }";
+        String deviceConfiguration = "{\r\n"
+                + "  \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\r\n"
+                + "    \"DeviceConfiguration\": {\r\n"
+                + "      \"properties\": {\r\n"
+                + "        \"roleName\": { \"type\": \"string\" }\r\n"
+                + "      }\r\n"
+                + "    }\r\n"
+                + "}";
+        Map<String, String> schemas = new HashMap<>();
+        schemas.put("https://www.example.org/PrimaryDeviceConfiguration.json", primaryDeviceConfiguration);
+        schemas.put("https://www.example.org/DeviceConfiguration.json", deviceConfiguration);
+        JsonSchema schema = JsonSchemaFactory
+                .getInstance(VersionFlag.V201909,
+                        builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas)))
+                .getSchema(schemaData);
+        String inputData = "{ \"isPrimaryDevice\": true, \"roleName\": \"hello\" }";
+        OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL,
+                executionContext -> {
+                    executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false);
+                    executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true);
+                });
+        assertTrue(outputUnit.isValid());
+    }
 }