Repository: networknt/json-schema-validator
Pull request #552 (2022-04-08 02:37:02)

Add schemaPath to ValidationMessage.
https://github.com/networknt/json-schema-validator/issues/550


Patch:
diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java
index c8196a8..ac84a1f 100644
--- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java
+++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java
@@ -34,11 +34,11 @@ public abstract class AbstractJsonValidator implements JsonValidator {
     }
 
     protected ValidationMessage buildValidationMessage(ErrorMessageType errorMessageType, String at, String... arguments) {
-        return ValidationMessage.of(keyword, errorMessageType, at, 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, details);
+        return ValidationMessage.of(keyword, errorMessageType, at, null, details);
     }
 
     protected Set<ValidationMessage> pass() {
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 78cce6d..7ffa4ba 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -137,7 +137,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
     }
 
     protected ValidationMessage buildValidationMessage(String at, String... arguments) {
-        final ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(), errorMessageType, at, arguments);
+        final ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(), errorMessageType, at, schemaPath, arguments);
         if (failFast && !isPartOfOneOfMultipleType()) {
             throw new JsonSchemaException(message);
         }
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 15b181c..ea29a8e 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -108,6 +108,7 @@ public class JsonSchema extends BaseJsonValidator {
                 throw new JsonSchemaException(ValidationMessage.of(ValidatorTypeCode.ID.getValue(),
                         ValidatorTypeCode.ID,
                         id,
+                        schemaPath,
                         currentUri == null ? "null" : currentUri.toString()));
             }
         }
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index 0c576b2..277ee18 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -312,7 +312,7 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
             msg = msg.concat(schemaValue);
         }
 
-        return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF, at, msg);
+        return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF, at, schemaPath, msg);
     }
 
     @Override
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index 74d2f73..b0555ff 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -46,7 +46,7 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
         if (schema == null) {
             throw new JsonSchemaException(
                     ValidationMessage.of(ValidatorTypeCode.REF.getValue(),
-                                                               CustomErrorMessageType.of("internal.unresolvedRef", new MessageFormat("{0}: Reference {1} cannot be resolved")), schemaPath, refValue));
+                                                               CustomErrorMessageType.of("internal.unresolvedRef", new MessageFormat("{0}: Reference {1} cannot be resolved")), schemaPath, schemaPath, refValue));
         }
     }
 
diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java
index b68ec56..843e4fe 100644
--- a/src/main/java/com/networknt/schema/ValidationMessage.java
+++ b/src/main/java/com/networknt/schema/ValidationMessage.java
@@ -26,6 +26,7 @@ public class ValidationMessage {
     private String type;
     private String code;
     private String path;
+    private String schemaPath;
     private String[] arguments;
     private Map<String, Object> details;
     private String message;
@@ -41,6 +42,9 @@ public class ValidationMessage {
         this.code = code;
     }
 
+    /**
+     * @return The path to the input json
+     */
     public String getPath() {
         return path;
     }
@@ -49,6 +53,17 @@ public class ValidationMessage {
         this.path = path;
     }
 
+    /**
+     * @return The path to the schema
+     */
+    public String getSchemaPath() {
+        return schemaPath;
+    }
+
+    public void setSchemaPath(String schemaPath) {
+        this.schemaPath = schemaPath;
+    }
+
     public String[] getArguments() {
         return arguments;
     }
@@ -88,6 +103,7 @@ public class ValidationMessage {
         if (type != null ? !type.equals(that.type) : that.type != null) return false;
         if (code != null ? !code.equals(that.code) : that.code != null) return false;
         if (path != null ? !path.equals(that.path) : that.path != null) return false;
+        if (schemaPath != null ? !schemaPath.equals(that.schemaPath) : that.schemaPath != null) return false;
         if (details != null ? !details.equals(that.details) : that.details != null) return false;
         if (!Arrays.equals(arguments, that.arguments)) return false;
         return !(message != null ? !message.equals(that.message) : that.message != null);
@@ -99,6 +115,7 @@ public class ValidationMessage {
         int result = type != null ? type.hashCode() : 0;
         result = 31 * result + (code != null ? code.hashCode() : 0);
         result = 31 * result + (path != null ? path.hashCode() : 0);
+        result = 31 * result + (schemaPath != null ? schemaPath.hashCode() : 0);
         result = 31 * result + (details != null ? details.hashCode() : 0);
         result = 31 * result + (arguments != null ? Arrays.hashCode(arguments) : 0);
         result = 31 * result + (message != null ? message.hashCode() : 0);
@@ -113,17 +130,17 @@ public class ValidationMessage {
         this.type = type;
     }
 
-    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String... arguments) {
+    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String schemaPath, String... arguments) {
         ValidationMessage.Builder builder = new ValidationMessage.Builder();
-        builder.code(errorMessageType.getErrorCode()).path(at).arguments(arguments)
+        builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).arguments(arguments)
                 .format(errorMessageType.getMessageFormat()).type(type)
                 .customMessage(errorMessageType.getCustomMessage());
         return builder.build();
     }
 
-    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, Map<String, Object> details) {
+    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String schemaPath, Map<String, Object> details) {
         ValidationMessage.Builder builder = new ValidationMessage.Builder();
-        builder.code(errorMessageType.getErrorCode()).path(at).details(details)
+        builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).details(details)
                 .format(errorMessageType.getMessageFormat()).type(type);
         return builder.build();
     }
@@ -132,6 +149,7 @@ public class ValidationMessage {
         private String type;
         private String code;
         private String path;
+        private String schemaPath;
         private String[] arguments;
         private Map<String, Object> details;
         private MessageFormat format;
@@ -152,6 +170,11 @@ public class ValidationMessage {
             return this;
         }
 
+        public Builder schemaPath(String schemaPath) {
+            this.schemaPath = schemaPath;
+            return this;
+        }
+
         public Builder arguments(String... arguments) {
             this.arguments = arguments;
             return this;
@@ -177,6 +200,7 @@ public class ValidationMessage {
             msg.setType(type);
             msg.setCode(code);
             msg.setPath(path);
+            msg.setSchemaPath(schemaPath);
             msg.setArguments(arguments);
             msg.setDetails(details);
 


Test patch:
diff --git a/src/test/java/com/networknt/schema/Issue550Test.java b/src/test/java/com/networknt/schema/Issue550Test.java
new file mode 100644
index 0000000..fc8e9fc
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue550Test.java
@@ -0,0 +1,55 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.Set;
+
+
+public class Issue550Test {
+    protected JsonSchema getJsonSchemaFromStreamContentV7(String schemaPath) {
+        InputStream schemaContent = getClass().getResourceAsStream(schemaPath);
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        return factory.getSchema(schemaContent);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(String dataPath) throws Exception {
+        InputStream content = getClass().getResourceAsStream(dataPath);
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = mapper.readTree(content);
+        return node;
+    }
+
+    @Test
+    void testValidationMessageDoContainSchemaPath() throws Exception {
+        String schemaPath = "/schema/issue500_1-v7.json";
+        String dataPath = "/data/issue500_1.json";
+        JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataPath);
+
+        Set<ValidationMessage> errors = schema.validate(node);
+        ValidationMessage validationMessage = errors.stream().findFirst().get();
+
+        Assertions.assertEquals("#/properties/age/minimum", validationMessage.getSchemaPath());
+        Assertions.assertEquals(1, errors.size());
+    }
+
+    @Test
+    void testValidationMessageDoContainSchemaPathForOneOf() throws Exception {
+        String schemaPath = "/schema/issue500_2-v7.json";
+        String dataPath = "/data/issue500_2.json";
+        JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataPath);
+
+        Set<ValidationMessage> errors = schema.validate(node);
+        ValidationMessage validationMessage = errors.stream().findFirst().get();
+
+        // Instead of capturing all subSchema within oneOf, a pointer to oneOf should be provided.
+        Assertions.assertEquals("#/oneOf", validationMessage.getSchemaPath());
+        Assertions.assertEquals(1, errors.size());
+    }
+
+}
diff --git a/src/test/resources/data/issue500_1.json b/src/test/resources/data/issue500_1.json
new file mode 100644
index 0000000..f9ae1d7
--- /dev/null
+++ b/src/test/resources/data/issue500_1.json
@@ -0,0 +1,5 @@
+{
+  "firstName": "John",
+  "lastName": "Doe",
+  "age": -21
+}
diff --git a/src/test/resources/data/issue500_2.json b/src/test/resources/data/issue500_2.json
new file mode 100644
index 0000000..2ff89e6
--- /dev/null
+++ b/src/test/resources/data/issue500_2.json
@@ -0,0 +1,5 @@
+{
+  "firstName": "John",
+  "lastName": "Doe",
+  "age": 15
+}
diff --git a/src/test/resources/schema/issue500_1-v7.json b/src/test/resources/schema/issue500_1-v7.json
new file mode 100644
index 0000000..c88c294
--- /dev/null
+++ b/src/test/resources/schema/issue500_1-v7.json
@@ -0,0 +1,18 @@
+{
+  "$id": "https://example.com/person.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Person",
+  "type": "object",
+  "properties": {
+    "firstName": {
+      "type": "string"
+    },
+    "lastName": {
+      "type": "string"
+    },
+    "age": {
+      "type": "integer",
+      "minimum": 0
+    }
+  }
+}
diff --git a/src/test/resources/schema/issue500_2-v7.json b/src/test/resources/schema/issue500_2-v7.json
new file mode 100644
index 0000000..934c606
--- /dev/null
+++ b/src/test/resources/schema/issue500_2-v7.json
@@ -0,0 +1,35 @@
+{
+  "$id": "https://example.com/person.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Person",
+  "type": "object",
+  "properties": {
+    "firstName": {
+      "type": "string"
+    },
+    "lastName": {
+      "type": "string"
+    },
+    "age": {
+      "type": "integer",
+      "minimum": 0
+    }
+  },
+  "oneOf": [
+    {
+      "properties": {
+        "firstName": {
+          "const": "John"
+        }
+      }
+    },
+    {
+      "properties": {
+        "lastName": {
+          "const": "Doe"
+        }
+      }
+    }
+  ]
+}
+