Repository: networknt/json-schema-validator
Pull request #689 (2023-03-31 13:45:18)

#687 Support for (valid) JSONPath and JSONPointer expressions as message paths
Implemented the feature proposed in #687. Note that this update would also resolve issue #567.

Note that while doing this update I took the liberty of making a small cleanup linked to the (barely) used `AbstractJsonValidator` class. This is used in only one place really (apart from unit tests), specifically for the (non) validation of keywords.

codecov-commenter: ## [Codecov](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) Report
> Merging [#689](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) (b909305) into [master](https://codecov.io/gh/networknt/json-schema-validator/commit/5a67ecc1171ed59ac6febdff247003b5ae37ea75?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) (5a67ecc) will **increase** coverage by `0.17%`.
> The diff coverage is `78.08%`.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt)

```diff
@@             Coverage Diff              @@
##             master     #689      +/-   ##
============================================
+ Coverage     72.43%   72.60%   +0.17%     
- Complexity     1015     1035      +20     
============================================
  Files           101      102       +1     
  Lines          3762     3786      +24     
  Branches        782      786       +4     
============================================
+ Hits           2725     2749      +24     
+ Misses          750      746       -4     
- Partials        287      291       +4     
```


| [Impacted Files](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) | Coverage Δ | |
|---|---|---|
| [...va/com/networknt/schema/AbstractJsonValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9BYnN0cmFjdEpzb25WYWxpZGF0b3IuamF2YQ==) | `83.33% <0.00%> (+14.10%)` | :arrow_up: |
| [.../main/java/com/networknt/schema/JsonValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9Kc29uVmFsaWRhdG9yLmphdmE=) | `100.00% <ø> (ø)` | |
| [...n/java/com/networknt/schema/BaseJsonValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9CYXNlSnNvblZhbGlkYXRvci5qYXZh) | `76.80% <50.00%> (+0.13%)` | :arrow_up: |
| [...ava/com/networknt/schema/PrefixItemsValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9QcmVmaXhJdGVtc1ZhbGlkYXRvci5qYXZh) | `47.19% <50.00%> (ø)` | |
| [src/main/java/com/networknt/schema/JsonSchema.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9Kc29uU2NoZW1hLmphdmE=) | `87.55% <57.14%> (-0.38%)` | :arrow_down: |
| [...main/java/com/networknt/schema/ItemsValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9JdGVtc1ZhbGlkYXRvci5qYXZh) | `74.15% <66.66%> (ø)` | |
| [...etworknt/schema/AdditionalPropertiesValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9BZGRpdGlvbmFsUHJvcGVydGllc1ZhbGlkYXRvci5qYXZh) | `75.00% <75.00%> (ø)` | |
| [src/main/java/com/networknt/schema/PathType.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9QYXRoVHlwZS5qYXZh) | `95.65% <95.65%> (ø)` | |
| [...n/java/com/networknt/schema/ContainsValidator.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9Db250YWluc1ZhbGlkYXRvci5qYXZh) | `72.00% <100.00%> (ø)` | |
| [...ava/com/networknt/schema/NonValidationKeyword.java](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt#diff-c3JjL21haW4vamF2YS9jb20vbmV0d29ya250L3NjaGVtYS9Ob25WYWxpZGF0aW9uS2V5d29yZC5qYXZh) | `100.00% <100.00%> (ø)` | |
| ... and [5 more](https://codecov.io/gh/networknt/json-schema-validator/pull/689?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=networknt)


Patch:
diff --git a/doc/config.md b/doc/config.md
index 3aa6c83..0d0886e 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -63,3 +63,10 @@ When set to true, can interpret round doubles as integers.
 Note that setting `javaSemantics = true` will achieve the same functionality at this time.
 
 For more details, please refer to this [issue](https://github.com/networknt/json-schema-validator/issues/344).
+
+* pathType
+
+This defines how path expressions are defined and returned once validation is performed through `ValidationMessage` instances. This can either be set to `PathType.JSON_POINTER` for [JSONPointer](https://www.rfc-editor.org/rfc/rfc6901.html) expressions,
+or to `PathType.JSON_PATH` for [JSONPath](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-base/) expressions. Doing so allows you to report the path for each finding and to potentially lookup nodes
+(see [here](https://github.com/networknt/json-schema-validator/blob/c41df270a71f8423c63cfaa379d2e9b3f570b73e/doc/yaml-line-numbers.md#scenario-2---validationmessage-line-locations) for an example). By default, path expressions use a
+`PathType.LEGACY` format which is close to JSONPath but does not escape reserved characters.
diff --git a/doc/yaml-line-numbers.md b/doc/yaml-line-numbers.md
index 2d5f06e..2894bd6 100644
--- a/doc/yaml-line-numbers.md
+++ b/doc/yaml-line-numbers.md
@@ -266,18 +266,24 @@ There is still a problem though, what if the validation against the schema fails
 
 Any failures validation against the schema come back in the form of a set of `ValidationMessage` objects. But these also do not contain original YAML source line information, and there's no easy way to inject it as we did for Scenario 1. Luckily though, there is a trick we can use here!
 
-Within the `ValidationMessage` object is something called the 'path' of the error, which we can access with the `getPath()` method. The syntax of this path is not exactly the same as a regular [JsonPointer](https://fasterxml.github.io/jackson-core/javadoc/2.10/com/fasterxml/jackson/core/JsonPointer.html) object, but it is sufficiently close as to be convertible. And, once converted, we can use that pointer for locating the appropriate `JsonNode`. The following couple of methods can be used to automate this process
+Within the `ValidationMessage` object is something called the 'path' of the error, which we can access with the `getPath()` method. The syntax of this path by default is close to being [JSONPath](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-base/), but can be set explicitly to be
+either [JSONPath](https://datatracker.ietf.org/doc/draft-ietf-jsonpath-base/) or [JSONPointer](https://www.rfc-editor.org/rfc/rfc6901.html) expressions. In our case as we already use [Jackson](https://github.com/FasterXML/jackson) which supports node lookups based on JSONPointer expressions,
+we will set the path expressions to be JSONPointers. This is achieved by configuring the reported path type through the `SchemaValidatorsConfig` before we read our schema:
+
+```java
+    SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+    config.setPathType(PathType.JSON_POINTER);
+    JsonSchema jsonSchema = JsonSchemaFactory.getInstance().getSchema(schema, config);
+```
+
+Having set paths to be JSONPointer expressions we can use those pointers for locating the appropriate `JsonNode` instances. The following couple of methods illustrate this process:
 
 ```java
     JsonNode findJsonNode(ValidationMessage msg, JsonNode rootNode)
     {
-        // munge the ValidationMessage path
-        String pathStr = StringUtils.replace(msg.getPath(), "$.", "/", 1);
-        pathStr = StringUtils.replace(pathStr, ".", "/");
-        pathStr = StringUtils.replace(pathStr, "[", "/");
-        pathStr = StringUtils.replace(pathStr, "]", "");  // array closure superfluous
-        JsonPointer pathPtr = JsonPointer.valueOf(pathStr);
-        // Now see if we can find the node
+        // Construct the JSONPointer.
+        JsonPointer pathPtr = JsonPointer.valueOf(msg.getPath());
+        // Now see if we can find the node.
         JsonNode node = rootNode.at(pathPtr);
         return node;
     }
diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java
index ac84a1f..a381009 100644
--- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java
+++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java
@@ -19,40 +19,14 @@ package com.networknt.schema;
 import com.fasterxml.jackson.databind.JsonNode;
 
 import java.util.Collections;
-import java.util.Map;
 import java.util.Set;
 
 public abstract class AbstractJsonValidator implements JsonValidator {
-    private final String keyword;
-
-    protected AbstractJsonValidator(String keyword) {
-        this.keyword = keyword;
-    }
 
     public Set<ValidationMessage> validate(JsonNode node) {
-        return validate(node, node, AT_ROOT);
-    }
-
-    protected ValidationMessage buildValidationMessage(ErrorMessageType errorMessageType, String at, String... arguments) {
-        return ValidationMessage.of(keyword, errorMessageType, at, null, arguments);
-    }
-
-    protected ValidationMessage buildValidationMessage(ErrorMessageType errorMessageType, String at, Map<String, Object> details) {
-        return ValidationMessage.of(keyword, errorMessageType, at, null, details);
+        return validate(node, node, PathType.LEGACY.getRoot());
     }
 
-    protected Set<ValidationMessage> pass() {
-        return Collections.emptySet();
-    }
-
-    protected Set<ValidationMessage> fail(ErrorMessageType errorMessageType, String at, Map<String, Object> details) {
-        return Collections.singleton(buildValidationMessage(errorMessageType, at, details));
-    }
-
-    protected Set<ValidationMessage> fail(ErrorMessageType errorMessageType, String at, String... arguments) {
-        return Collections.singleton(buildValidationMessage(errorMessageType, at, arguments));
-    }
-    
     @Override
 	public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
 		Set<ValidationMessage> validationMessages = Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
index 9b61a1f..c91dc75 100644
--- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
@@ -88,7 +88,7 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
         // if allowAdditionalProperties is true, add all the properties as evaluated.
         if (allowAdditionalProperties) {
             for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
-                addToEvaluatedProperties(at + "." + it.next());
+                addToEvaluatedProperties(atPath(at, it.next()));
             }
         }
 
@@ -114,9 +114,9 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
                     if (additionalPropertiesSchema != null) {
                         ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
                         if (state != null && state.isWalkEnabled()) {
-                            errors.addAll(additionalPropertiesSchema.walk(node.get(pname), rootNode, at + "." + pname, state.isValidationEnabled()));
+                            errors.addAll(additionalPropertiesSchema.walk(node.get(pname), rootNode, atPath(at, pname), state.isValidationEnabled()));
                         } else {
-                            errors.addAll(additionalPropertiesSchema.validate(node.get(pname), rootNode, at + "." + pname));
+                            errors.addAll(additionalPropertiesSchema.validate(node.get(pname), rootNode, atPath(at, pname)));
                         }
                     }
                 }
@@ -157,7 +157,7 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
                     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());
+                           additionalPropertiesSchema.walk(node.get(pname), rootNode, atPath(at, pname), state.isValidationEnabled());
                         }
                     }
                 }
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 284a362..49a07db 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -40,19 +40,14 @@ public abstract class BaseJsonValidator implements JsonValidator {
     protected final boolean failFast;
     protected final ApplyDefaultsStrategy applyDefaultsStrategy;
     private final String customMessage;
+    private final PathType pathType;
 
     public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
                              ValidatorTypeCode validatorType, ValidationContext validationContext) {
         this(schemaPath, schemaNode, parentSchema, validatorType, false,
              validationContext.getConfig() != null && validationContext.getConfig().isFailFast(),
-             validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null);
-    }
-
-    // TODO: can this be made package private?
-    @Deprecated // use the BaseJsonValidator below
-    public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
-                             ValidatorTypeCode validatorType, boolean suppressSubSchemaRetrieval, boolean failFast) {
-        this(schemaPath, schemaNode, parentSchema, validatorType, false, failFast, null);
+             validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null,
+            validationContext.getConfig() != null ? validationContext.getConfig().getPathType() : null);
     }
 
     public BaseJsonValidator(String schemaPath,
@@ -61,7 +56,8 @@ public abstract class BaseJsonValidator implements JsonValidator {
                              ValidatorTypeCode validatorType,
                              boolean suppressSubSchemaRetrieval,
                              boolean failFast,
-                             ApplyDefaultsStrategy applyDefaultsStrategy) {
+                             ApplyDefaultsStrategy applyDefaultsStrategy,
+                             PathType pathType) {
         this.errorMessageType = validatorType;
         this.schemaPath = schemaPath;
         this.schemaNode = schemaNode;
@@ -75,6 +71,11 @@ public abstract class BaseJsonValidator implements JsonValidator {
         } else {
             this.customMessage = null;
         }
+        if (pathType != null) {
+            this.pathType = pathType;
+        } else {
+            this.pathType = PathType.DEFAULT;
+        }
     }
 
     public String getSchemaPath() {
@@ -117,7 +118,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
     }
 
     public Set<ValidationMessage> validate(JsonNode node) {
-        return validate(node, node, AT_ROOT);
+        return validate(node, node, atRoot());
     }
 
     protected boolean equals(double n1, double n2) {
@@ -189,6 +190,37 @@ public abstract class BaseJsonValidator implements JsonValidator {
         return parentSchema.schemaPath.contains("/" + ValidatorTypeCode.ONE_OF.getValue() + "/");
     }
 
+    /**
+     * Get the root path.
+     *
+     * @return The path.
+     */
+    protected String atRoot() {
+        return pathType.getRoot();
+    }
+
+    /**
+     * Create the path for a given child token.
+     *
+     * @param currentPath The current path.
+     * @param token The child token.
+     * @return The complete path.
+     */
+    protected String atPath(String currentPath, String token) {
+        return pathType.append(currentPath, token);
+    }
+
+    /**
+     * Create the path for a given child indexed item.
+     *
+     * @param currentPath The current path.
+     * @param index The child index.
+     * @return The complete path.
+     */
+    protected String atPath(String currentPath, int index) {
+        return pathType.append(currentPath, index);
+    }
+
     /* ********************** START OF OpenAPI 3.0.x DISCRIMINATOR METHODS ********************************* */
 
     /**
diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java
index 97babf0..3db2c2b 100644
--- a/src/main/java/com/networknt/schema/ContainsValidator.java
+++ b/src/main/java/com/networknt/schema/ContainsValidator.java
@@ -55,7 +55,7 @@ public class ContainsValidator extends BaseJsonValidator implements JsonValidato
         } else if (node.isArray()) {
             int i = 0;
             for (JsonNode n : node) {
-                if (schema.validate(n, rootNode, at + "[" + i + "]").isEmpty()) {
+                if (schema.validate(n, rootNode, atPath(at, i)).isEmpty()) {
                     //Short circuit on first success
                     return Collections.emptySet();
                 }
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 4af2b1b..b0d761e 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -96,17 +96,17 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
         if (schema != null) {
             // validate with item schema (the whole array has the same item
             // schema)
-            errors.addAll(schema.validate(node, rootNode, at + "[" + i + "]"));
+            errors.addAll(schema.validate(node, rootNode, atPath(at, i)));
         }
 
         if (tupleSchema != null) {
             if (i < tupleSchema.size()) {
                 // validate against tuple schema
-                errors.addAll(tupleSchema.get(i).validate(node, rootNode, at + "[" + i + "]"));
+                errors.addAll(tupleSchema.get(i).validate(node, rootNode, atPath(at, i)));
             } else {
                 if (additionalSchema != null) {
                     // validate against additional item schema
-                    errors.addAll(additionalSchema.validate(node, rootNode, at + "[" + i + "]"));
+                    errors.addAll(additionalSchema.validate(node, rootNode, atPath(at, i)));
                 } else if (!additionalItems) {
                     // no additional item allowed, return error
                     errors.add(buildValidationMessage(at, "" + i));
@@ -143,18 +143,18 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
             String at, boolean shouldValidateSchema) {
         if (schema != null) {
             // Walk the schema.
-            walkSchema(schema, node, rootNode, at + "[" + i + "]", shouldValidateSchema, validationMessages);
+            walkSchema(schema, node, rootNode, atPath(at, i), shouldValidateSchema, validationMessages);
         }
 
         if (tupleSchema != null) {
             if (i < tupleSchema.size()) {
                 // walk tuple schema
-                walkSchema(tupleSchema.get(i), node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                walkSchema(tupleSchema.get(i), node, rootNode, atPath(at, i), shouldValidateSchema,
                         validationMessages);
             } else {
                 if (additionalSchema != null) {
                     // walk additional item schema
-                    walkSchema(additionalSchema, node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                    walkSchema(additionalSchema, node, rootNode, atPath(at, i), shouldValidateSchema,
                             validationMessages);
                 }
             }
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 6cd25db..4d5c3aa 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -76,8 +76,9 @@ public class JsonSchema extends BaseJsonValidator {
     private JsonSchema(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode,
                        JsonSchema parent, boolean suppressSubSchemaRetrieval) {
         super(schemaPath, schemaNode, parent, null, suppressSubSchemaRetrieval,
-              validationContext.getConfig() != null && validationContext.getConfig().isFailFast(),
-              validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null);
+            validationContext.getConfig() != null && validationContext.getConfig().isFailFast(),
+            validationContext.getConfig() != null ? validationContext.getConfig().getApplyDefaultsStrategy() : null,
+            validationContext.getConfig() != null ? validationContext.getConfig().getPathType() : null);
         this.validationContext = validationContext;
         this.metaSchema = validationContext.getMetaSchema();
         this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode);
@@ -298,7 +299,7 @@ public class JsonSchema extends BaseJsonValidator {
     @Override
     public Set<ValidationMessage> validate(JsonNode node) {
         try {
-            Set<ValidationMessage> errors = validate(node, node, AT_ROOT);
+            Set<ValidationMessage> errors = validate(node, node, atRoot());
             return errors;
         } finally {
             if (validationContext.getConfig().isResetCollectorContext()) {
@@ -352,7 +353,7 @@ public class JsonSchema extends BaseJsonValidator {
     }
 
     public ValidationResult validateAndCollect(JsonNode node) {
-        return validateAndCollect(node, node, AT_ROOT);
+        return validateAndCollect(node, node, atRoot());
     }
 
     /**
@@ -402,7 +403,7 @@ public class JsonSchema extends BaseJsonValidator {
      * @return result of ValidationResult
      */
     public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) {
-        return walkAtNodeInternal(node, node, AT_ROOT, shouldValidateSchema);
+        return walkAtNodeInternal(node, node, atRoot(), shouldValidateSchema);
     }
 
     public ValidationResult walkAtNode(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
@@ -425,7 +426,7 @@ public class JsonSchema extends BaseJsonValidator {
                 collectorContext.loadCollectors();
             }
             // Process UnEvaluatedProperties after all the validators are called.
-            errors.addAll(processUnEvaluatedProperties(node, node, AT_ROOT, shouldValidateSchema, false));
+            errors.addAll(processUnEvaluatedProperties(node, node, atRoot(), shouldValidateSchema, false));
             // Collect errors and collector context into validation result.
             ValidationResult validationResult = new ValidationResult(errors, collectorContext);
             return validationResult;
diff --git a/src/main/java/com/networknt/schema/JsonValidator.java b/src/main/java/com/networknt/schema/JsonValidator.java
index dafc279..4f71fbb 100644
--- a/src/main/java/com/networknt/schema/JsonValidator.java
+++ b/src/main/java/com/networknt/schema/JsonValidator.java
@@ -25,7 +25,6 @@ import com.networknt.schema.walk.JsonSchemaWalker;
  * Standard json validator interface, implemented by all validators and JsonSchema.
  */
 public interface JsonValidator extends JsonSchemaWalker {
-    String AT_ROOT = "$";
 
     /**
      * Validate the given root JsonNode, starting at the root of the data path.
diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java
index 80271ac..4bb6e39 100644
--- a/src/main/java/com/networknt/schema/NonValidationKeyword.java
+++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java
@@ -27,10 +27,6 @@ import java.util.Set;
 public class NonValidationKeyword extends AbstractKeyword {
 
     private static final class Validator extends AbstractJsonValidator {
-        private Validator(String keyword) {
-            super(keyword);
-        }
-
         @Override
         public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
             return Collections.emptySet();
@@ -44,6 +40,6 @@ public class NonValidationKeyword extends AbstractKeyword {
     @Override
     public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
                                       ValidationContext validationContext) throws JsonSchemaException, Exception {
-        return new Validator(getValue());
+        return new Validator();
     }
 }
diff --git a/src/main/java/com/networknt/schema/PathType.java b/src/main/java/com/networknt/schema/PathType.java
new file mode 100644
index 0000000..4692c61
--- /dev/null
+++ b/src/main/java/com/networknt/schema/PathType.java
@@ -0,0 +1,99 @@
+package com.networknt.schema;
+
+import java.util.function.Function;
+
+/**
+ * Enumeration defining the different approached available to generate the paths added to validation messages.
+ */
+public enum PathType {
+
+    /**
+     * The legacy approach, loosely based on JSONPath (but not guaranteed to give valid JSONPath expressions).
+     */
+    LEGACY("$", (token) -> "." + token, (index) -> "[" + index + "]"),
+    /**
+     * Paths as JSONPath expressions.
+     */
+    JSON_PATH("$", (token) -> {
+        /*
+         * Accepted characters for shorthand paths:
+         * - 'a' through 'z'
+         * - 'A' through 'Z'
+         * - '0' through '9'
+         * - Underscore ('_')
+         */
+        if (token.codePoints().allMatch(c -> (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_')) {
+            return "." + token;
+        } else {
+            return "[\"" + token + "\"]";
+        }
+    }, (index) -> "[" + index + "]"),
+    /**
+     * Paths as JSONPointer expressions.
+     */
+    JSON_POINTER("", (token) -> {
+        /*
+         * Escape '~' with '~0' and '/' with '~1'.
+         */
+        if (token.indexOf('~') != -1) {
+            token = token.replace("~", "~0");
+        }
+        if (token.indexOf('/') != -1) {
+            token = token.replace("/", "~1");
+        }
+        return "/" + token;
+    }, (index) -> "/" + index);
+
+    /**
+     * The default path generation approach to use.
+     */
+    public static final PathType DEFAULT = LEGACY;
+    private final String rootToken;
+    private final Function<String, String> appendTokenFn;
+    private final Function<Integer, String> appendIndexFn;
+
+    /**
+     * Constructor.
+     *
+     * @param rootToken The token representing the document root.
+     * @param appendTokenFn A function used to define the path fragment used to append a token (e.g. property) to an existing path.
+     * @param appendIndexFn A function used to append an index (for arrays) to an existing path.
+     */
+    PathType(String rootToken, Function<String, String> appendTokenFn, Function<Integer, String> appendIndexFn) {
+        this.rootToken = rootToken;
+        this.appendTokenFn = appendTokenFn;
+        this.appendIndexFn = appendIndexFn;
+    }
+
+    /**
+     * Append the given child token to the provided current path.
+     *
+     * @param currentPath The path to append to.
+     * @param child The child token.
+     * @return The resulting complete path.
+     */
+    public String append(String currentPath, String child) {
+        return currentPath + appendTokenFn.apply(child);
+    }
+
+    /**
+     * Append the given index to the provided current path.
+     *
+     * @param currentPath The path to append to.
+     * @param index The index to append.
+     * @return The resulting complete path.
+     */
+    public String append(String currentPath, int index) {
+        return currentPath + appendIndexFn.apply(index);
+    }
+
+    /**
+     * Return the representation of the document root.
+     *
+     * @return The root token.
+     */
+    public String getRoot() {
+        return rootToken;
+    }
+
+}
diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
index 641eadb..66f5e32 100644
--- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
@@ -58,8 +58,8 @@ public class PatternPropertiesValidator extends BaseJsonValidator implements Jso
             for (Map.Entry<Pattern, JsonSchema> entry : schemas.entrySet()) {
                 Matcher m = entry.getKey().matcher(name);
                 if (m.find()) {
-                    addToEvaluatedProperties(at + "." + name);
-                    errors.addAll(entry.getValue().validate(n, rootNode, at + "." + name));
+                    addToEvaluatedProperties(atPath(at, name));
+                    errors.addAll(entry.getValue().validate(n, rootNode, atPath(at, name)));
                 }
             }
         }
diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
index aec1839..8eb90c3 100644
--- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java
+++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
@@ -100,17 +100,17 @@ public class PrefixItemsValidator extends BaseJsonValidator implements JsonValid
         if (schema != null) {
             // validate with item schema (the whole array has the same item
             // schema)
-            errors.addAll(schema.validate(node, rootNode, at + "[" + i + "]"));
+            errors.addAll(schema.validate(node, rootNode, atPath(at, i)));
         }
 
         if (tupleSchema != null) {
             if (i < tupleSchema.size()) {
                 // validate against tuple schema
-                errors.addAll(tupleSchema.get(i).validate(node, rootNode, at + "[" + i + "]"));
+                errors.addAll(tupleSchema.get(i).validate(node, rootNode, atPath(at, i)));
             } else {
                 if (additionalSchema != null) {
                     // validate against additional item schema
-                    errors.addAll(additionalSchema.validate(node, rootNode, at + "[" + i + "]"));
+                    errors.addAll(additionalSchema.validate(node, rootNode, atPath(at, i)));
                 } else if (!additionalItems) {
                     // no additional item allowed, return error
                     errors.add(buildValidationMessage(at, "" + i));
@@ -147,18 +147,18 @@ public class PrefixItemsValidator extends BaseJsonValidator implements JsonValid
             String at, boolean shouldValidateSchema) {
         if (schema != null) {
             // Walk the schema.
-            walkSchema(schema, node, rootNode, at + "[" + i + "]", shouldValidateSchema, validationMessages);
+            walkSchema(schema, node, rootNode, atPath(at, i), shouldValidateSchema, validationMessages);
         }
 
         if (tupleSchema != null) {
             if (i < tupleSchema.size()) {
                 // walk tuple schema
-                walkSchema(tupleSchema.get(i), node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                walkSchema(tupleSchema.get(i), node, rootNode, atPath(at, i), shouldValidateSchema,
                         validationMessages);
             } else {
                 if (additionalSchema != null) {
                     // walk additional item schema
-                    walkSchema(additionalSchema, node, rootNode, at + "[" + i + "]", shouldValidateSchema,
+                    walkSchema(additionalSchema, node, rootNode, atPath(at, i), shouldValidateSchema,
                             validationMessages);
                 }
             }
diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java
index 49f0f8b..9fe84a1 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -54,7 +54,7 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
             JsonSchema propertySchema = entry.getValue();
             JsonNode propertyNode = node.get(entry.getKey());
             if (propertyNode != null) {
-                addToEvaluatedProperties(at + "." + entry.getKey());
+                addToEvaluatedProperties(atPath(at, entry.getKey()));
                 // check whether this is a complex validator. save the state
                 boolean isComplex = state.isComplexValidator();
                // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated
@@ -66,7 +66,7 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
                 
                 if (!state.isWalkEnabled()) {
                     //validate the child element(s)
-                    errors.addAll(propertySchema.validate(propertyNode, rootNode, at + "." + entry.getKey()));
+                    errors.addAll(propertySchema.validate(propertyNode, rootNode, atPath(at, entry.getKey())));
                 } else {
                     // check if walker is enabled. If it is enabled it is upto the walker implementation to decide about the validation.
                     walkSchema(entry, node, rootNode, at, state.isValidationEnabled(), errors, propertyWalkListenerRunner);
@@ -155,15 +155,15 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
         JsonSchema propertySchema = entry.getValue();
         JsonNode propertyNode = (node == null ? null : node.get(entry.getKey()));
         boolean executeWalk = propertyWalkListenerRunner.runPreWalkListeners(ValidatorTypeCode.PROPERTIES.getValue(),
-                propertyNode, rootNode, at + "." + entry.getKey(), propertySchema.getSchemaPath(),
+                propertyNode, rootNode, atPath(at, entry.getKey()), propertySchema.getSchemaPath(),
                 propertySchema.getSchemaNode(), propertySchema.getParentSchema(), validationContext,
                 validationContext.getJsonSchemaFactory());
         if (executeWalk) {
             validationMessages.addAll(
-                    propertySchema.walk(propertyNode, rootNode, at + "." + entry.getKey(), shouldValidateSchema));
+                    propertySchema.walk(propertyNode, rootNode, atPath(at, entry.getKey()), shouldValidateSchema));
         }
         propertyWalkListenerRunner.runPostWalkListeners(ValidatorTypeCode.PROPERTIES.getValue(), propertyNode, rootNode,
-                at + "." + entry.getKey(), propertySchema.getSchemaPath(), propertySchema.getSchemaNode(),
+                atPath(at, entry.getKey()), propertySchema.getSchemaPath(), propertySchema.getSchemaNode(),
                 propertySchema.getParentSchema(), validationContext, validationContext.getJsonSchemaFactory(), validationMessages);
 
     }
diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
index 7a6bf84..41e4d5d 100644
--- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
@@ -41,7 +41,7 @@ public class PropertyNamesValidator extends BaseJsonValidator implements JsonVal
         for (Iterator<String> it = node.fieldNames(); it.hasNext(); ) {
             final String pname = it.next();
             final TextNode pnameText = TextNode.valueOf(pname);
-            final Set<ValidationMessage> schemaErrors = innerSchema.validate(pnameText, node, at + "." + pname);
+            final Set<ValidationMessage> schemaErrors = innerSchema.validate(pnameText, node, atPath(at, pname));
             for (final ValidationMessage schemaError : schemaErrors) {
                 final String path = schemaError.getPath();
                 String msg = schemaError.getMessage();
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index 07cf5aa..ac31a54 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -16,17 +16,13 @@
 
 package com.networknt.schema;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.uri.URITranslator;
 import com.networknt.schema.uri.URITranslator.CompositeURITranslator;
 import com.networknt.schema.walk.JsonSchemaWalkListener;
 
+import java.util.*;
+
 public class SchemaValidatorsConfig {
     /**
      * when validate type, if TYPE_LOOSE = true, will try to convert string to
@@ -102,6 +98,11 @@ public class SchemaValidatorsConfig {
      */
     private boolean writeMode = true;
 
+    /**
+     * The approach used to generate paths in reported messages, logs and errors. Default is the legacy "JSONPath-like" approach.
+     */
+    private PathType pathType = PathType.DEFAULT;
+
     // This is just a constant for listening to all Keywords.
     public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener";
 
@@ -357,4 +358,22 @@ public class SchemaValidatorsConfig {
     public void setWriteMode(boolean writeMode) {
         this.writeMode = writeMode;
     }
+
+    /**
+     * Set the approach used to generate paths in messages, logs and errors (default is PathType.LEGACY).
+     *
+     * @param pathType The path generation approach.
+     */
+    public void setPathType(PathType pathType) {
+        this.pathType = pathType;
+    }
+
+    /**
+     * Get the approach used to generate paths in messages, logs and errors.
+     *
+     * @return The path generation approach.
+     */
+    public PathType getPathType() {
+        return pathType;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
index 791f24a..9841c95 100644
--- a/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/UnEvaluatedPropertiesValidator.java
@@ -89,9 +89,9 @@ public class UnEvaluatedPropertiesValidator extends BaseJsonValidator implements
             String fieldName = nodesIterator.next();
             JsonNode jsonNode = node.get(fieldName);
             if (jsonNode.isObject()) {
-                processAllPaths(jsonNode, at + "." + fieldName, paths);
+                processAllPaths(jsonNode, atPath(at, fieldName), paths);
             }
-            paths.add(at + "." + fieldName);
+            paths.add(atPath(at, fieldName));
         }
     }
 


Test patch:
diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java
index be0e08c..da5b8dc 100644
--- a/src/test/java/com/networknt/schema/CollectorContextTest.java
+++ b/src/test/java/com/networknt/schema/CollectorContextTest.java
@@ -291,7 +291,7 @@ public class CollectorContextTest {
 
         @Override
         public Set<ValidationMessage> validate(JsonNode rootNode) {
-            return validate(rootNode, rootNode, BaseJsonValidator.AT_ROOT);
+            return validate(rootNode, rootNode, PathType.DEFAULT.getRoot());
         }
 
         @Override
@@ -368,7 +368,7 @@ public class CollectorContextTest {
 
         @Override
         public Set<ValidationMessage> validate(JsonNode rootNode) {
-            return validate(rootNode, rootNode, BaseJsonValidator.AT_ROOT);
+            return validate(rootNode, rootNode, PathType.DEFAULT.getRoot());
         }
 
         @Override
diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
index d1e0ac8..57ec222 100644
--- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
+++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
 import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -46,14 +47,16 @@ public class CustomMetaSchemaTest {
         private static final class Validator extends AbstractJsonValidator {
             private final List<String> enumValues;
             private final List<String> enumNames;
+            private final String keyword;
 
             private Validator(String keyword, List<String> enumValues, List<String> enumNames) {
-                super(keyword);
+                super();
                 if (enumNames.size() != enumValues.size()) {
                     throw new IllegalArgumentException("enum and enumNames need to be of same length");
                 }
                 this.enumNames = enumNames;
                 this.enumValues = enumValues;
+                this.keyword = keyword;
             }
 
             @Override
@@ -64,7 +67,9 @@ public class CustomMetaSchemaTest {
                     throw new IllegalArgumentException("value not found in enum. value: " + value + " enum: " + enumValues);
                 }
                 String valueName = enumNames.get(idx);
-                return fail(CustomErrorMessageType.of("tests.example.enumNames", new MessageFormat("{0}: enumName is {1}")), at, valueName);
+                Set<ValidationMessage> messages = new HashSet<>();
+                messages.add(ValidationMessage.of(keyword, CustomErrorMessageType.of("tests.example.enumNames", new MessageFormat("{0}: enumName is {1}")), at, null, valueName));
+                return messages;
             }
         }
 
diff --git a/src/test/java/com/networknt/schema/Issue687Test.java b/src/test/java/com/networknt/schema/Issue687Test.java
new file mode 100644
index 0000000..d8e3914
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue687Test.java
@@ -0,0 +1,91 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class Issue687Test {
+
+    @Test
+    void testRoot() {
+        assertEquals("$", PathType.LEGACY.getRoot());
+        assertEquals("$", PathType.JSON_PATH.getRoot());
+        assertEquals("", PathType.JSON_POINTER.getRoot());
+    }
+
+    @Test
+    void testDefault() {
+        assertEquals(PathType.LEGACY, PathType.DEFAULT);
+    }
+
+    public static Stream<Arguments> appendTokens() {
+        return Stream.of(
+                Arguments.of(PathType.LEGACY, "$.foo", "bar", "$.foo.bar"),
+                Arguments.of(PathType.LEGACY, "$.foo", "b.ar", "$.foo.b.ar"),
+                Arguments.of(PathType.LEGACY, "$.foo", "b~ar", "$.foo.b~ar"),
+                Arguments.of(PathType.LEGACY, "$.foo", "b/ar", "$.foo.b/ar"),
+                Arguments.of(PathType.JSON_PATH, "$.foo", "bar", "$.foo.bar"),
+                Arguments.of(PathType.JSON_PATH, "$.foo", "b.ar", "$.foo[\"b.ar\"]"),
+                Arguments.of(PathType.JSON_PATH, "$.foo", "b~ar", "$.foo[\"b~ar\"]"),
+                Arguments.of(PathType.JSON_PATH, "$.foo", "b/ar", "$.foo[\"b/ar\"]"),
+                Arguments.of(PathType.JSON_POINTER, "/foo", "bar", "/foo/bar"),
+                Arguments.of(PathType.JSON_POINTER, "/foo", "b.ar", "/foo/b.ar"),
+                Arguments.of(PathType.JSON_POINTER, "/foo", "b~ar", "/foo/b~0ar"),
+                Arguments.of(PathType.JSON_POINTER, "/foo", "b/ar", "/foo/b~1ar")
+        );
+    }
+
+    public static Stream<Arguments> appendIndexes() {
+        return Stream.of(
+                Arguments.of(PathType.LEGACY, "$.foo", 0, "$.foo[0]"),
+                Arguments.of(PathType.JSON_PATH, "$.foo", 0, "$.foo[0]"),
+                Arguments.of(PathType.JSON_POINTER, "/foo", 0, "/foo/0")
+        );
+    }
+
+    public static Stream<Arguments> validationMessages() {
+        String schemaPath = "/schema/issue687.json";
+        String content = "{ \"foo\": \"a\", \"b.ar\": 1, \"children\": [ { \"childFoo\": \"a\", \"c/hildBar\": 1 } ] }";
+        return Stream.of(
+                Arguments.of(PathType.LEGACY, schemaPath, content, new String[] { "$.b.ar", "$.children[0].c/hildBar" }),
+                Arguments.of(PathType.JSON_PATH, schemaPath, content, new String[] { "$[\"b.ar\"]", "$.children[0][\"c/hildBar\"]" }),
+                Arguments.of(PathType.JSON_POINTER, schemaPath, content, new String[] { "/b.ar", "/children/0/c~1hildBar" })
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("appendTokens")
+    void testAppendToken(PathType pathType, String currentPath, String token, String expected) {
+        assertEquals(expected, pathType.append(currentPath, token));
+    }
+
+    @ParameterizedTest
+    @MethodSource("appendIndexes")
+    void testAppendIndex(PathType pathType, String currentPath, Integer index, String expected) {
+        assertEquals(expected, pathType.append(currentPath, index));
+    }
+
+    @ParameterizedTest
+    @MethodSource("validationMessages")
+    void testValidationMessage(PathType pathType, String schemaPath, String content, String[] expectedMessagePaths) throws JsonProcessingException {
+        SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+        config.setPathType(pathType);
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
+        JsonSchema schema = factory.getSchema(Issue687Test.class.getResourceAsStream(schemaPath), config);
+        Set<ValidationMessage> messages = schema.validate(new ObjectMapper().readTree(content));
+        assertEquals(expectedMessagePaths.length, messages.size());
+        for (String expectedPath: expectedMessagePaths) {
+            assertTrue(messages.stream().anyMatch(msg -> expectedPath.equals(msg.getPath())));
+        }
+    }
+
+}
diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java
index 45025b3..535236f 100644
--- a/src/test/java/com/networknt/schema/JsonWalkTest.java
+++ b/src/test/java/com/networknt/schema/JsonWalkTest.java
@@ -158,7 +158,7 @@ public class JsonWalkTest {
 
             @Override
             public Set<ValidationMessage> validate(JsonNode rootNode) {
-                return validate(rootNode, rootNode, BaseJsonValidator.AT_ROOT);
+                return validate(rootNode, rootNode, PathType.DEFAULT.getRoot());
             }
 
             @Override
diff --git a/src/test/resources/schema/issue687.json b/src/test/resources/schema/issue687.json
new file mode 100644
index 0000000..5e24803
--- /dev/null
+++ b/src/test/resources/schema/issue687.json
@@ -0,0 +1,19 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "$id": "https://json-schema.org/draft/2019-09/schema",
+  "type": "object",
+  "properties": {
+    "foo": { "type": "string" },
+    "b.ar": { "type": "string" },
+    "children" : {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "childFoo": { "type": "string" },
+          "c/hildBar": { "type": "string" }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file