Repository: networknt/json-schema-validator
Pull request #634 (2023-01-17 20:28:50)

Quick fix for wrong custom message bug
The essence of the problem seems to be that keywords are unique and can only have one custom message. When there are multiple custom messages assigned to the same keyword in a schema the mutation of the custom message can result in the incorrect error messages to be output. 

This bug goes back to, at least, version 1.0.64.


Patch:
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index b06976e..094e6ec 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -39,6 +39,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
     protected ValidationContext validationContext;
     protected final boolean failFast;
     protected final ApplyDefaultsStrategy applyDefaultsStrategy;
+    private final String customMessage;
 
     public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
                              ValidatorTypeCode validatorType, ValidationContext validationContext) {
@@ -69,6 +70,11 @@ public abstract class BaseJsonValidator implements JsonValidator {
         this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval;
         this.failFast = failFast;
         this.applyDefaultsStrategy = applyDefaultsStrategy != null ? applyDefaultsStrategy : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;
+        if (validatorType != null) {
+            this.customMessage = validatorType.getCustomMessage();
+        } else {
+            this.customMessage = null;
+        }
     }
 
     public String getSchemaPath() {
@@ -137,7 +143,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
     }
 
     protected ValidationMessage buildValidationMessage(String at, String... arguments) {
-        final ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(), errorMessageType, at, schemaPath, arguments);
+        final ValidationMessage message = ValidationMessage.ofWithCustom(getValidatorType().getValue(), errorMessageType, customMessage, 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 22535ba..6cd25db 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -282,13 +282,15 @@ public class JsonSchema extends BaseJsonValidator {
         if (schemaNode.get("message") != null && schemaNode.get("message").get(pname) != null) {
             return schemaNode.get("message");
         }
-        JsonNode nodeContainingMessage;
-        if (parentSchema == null) {
-            nodeContainingMessage = schemaNode;
-        } else {
-            nodeContainingMessage = parentSchema.schemaNode;
+        JsonNode messageNode;
+        messageNode = schemaNode.get("message");
+        if (messageNode == null && parentSchema != null) {
+            messageNode = parentSchema.schemaNode.get("message");
+            if (messageNode == null) {
+                return getMessageNode(parentSchema.schemaNode, parentSchema.getParentSchema(), pname);
+            }
         }
-        return nodeContainingMessage.get("message");
+        return messageNode;
     }
 
     /************************ START OF VALIDATE METHODS **********************************/
diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java
index db14cab..8b4806e 100644
--- a/src/main/java/com/networknt/schema/ValidationMessage.java
+++ b/src/main/java/com/networknt/schema/ValidationMessage.java
@@ -130,14 +130,18 @@ public class ValidationMessage {
         this.type = type;
     }
 
-    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String schemaPath, String... arguments) {
+    public static ValidationMessage ofWithCustom(String type, ErrorMessageType errorMessageType, String customMessage, String at, String schemaPath, String... arguments) {
         ValidationMessage.Builder builder = new ValidationMessage.Builder();
         builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).arguments(arguments)
                 .format(errorMessageType.getMessageFormat()).type(type)
-                .customMessage(errorMessageType.getCustomMessage());
+                .customMessage(customMessage);
         return builder.build();
     }
 
+    public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String schemaPath, String... arguments) {
+        return ofWithCustom(type, errorMessageType, errorMessageType.getCustomMessage(), at, schemaPath, arguments);
+    }
+
     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).schemaPath(schemaPath).details(details)
diff --git a/src/main/resources/jsv-messages_fa_IR.properties b/src/main/resources/jsv-messages_fa_IR.properties
new file mode 100644
index 0000000..dbf5279
--- /dev/null
+++ b/src/main/resources/jsv-messages_fa_IR.properties
@@ -0,0 +1,43 @@
+$ref = {0}: دارای خطا با 'refs'
+additionalProperties = {0}.{1}: در طرحواره تعریف نشده است و طرح ویژگی های اضافی را اجازه نمی دهد
+allOf = {0}: باید برای همه طرحواره ها معتبر باشد {1}
+anyOf = {0}: باید برای هر یک از طرحواره ها معتبر باشد {1}
+const = {0}: باید برابر مقدار {1} باشد
+contains = {0}: حاوی عنصری نیست که از این اعتبار سنجی ها عبور کند: {1}
+crossEdits = {0}: دارای خطای «ویرایش متقاطع» است
+dateTime = {0}: {1} نامعتبر است {2}
+dependencies = {0}: دارای خطای وابستگی است {1}
+dependentRequired = {0}: دارای یک مشخصه گم شده است که اجباری است {1}
+dependentSchemas = {0}: دارای خطا با dependentSchemas است {1}
+edits = {0}: دارای خطا با 'ویرایش ها' است
+enum = {0}: مقدار در درون لیست مجازها نیست {1}
+exclusiveMaximum = {0}: باید حداکثر مقدار مطلق داشته باشد {1}
+exclusiveMinimum = {0}: باید حداقل مقدار انحصاری داشته باشد {1}
+false = طرحواره بولی نادرست معتبر نیست
+format = {0}: با الگوی {1} {2} مطابقت ندارد
+id = {0}: {1} یک بخش نامعتبر برای URI {2} است
+items = {0}[{1}]: هیچ اعتبارسنجی در این فهرست یافت نشد
+maxItems = {0}: باید حداکثر {1} مورد در آرایه وجود داشته باشد
+maxLength = {0}: ممکن است فقط {1} کاراکتر باشد
+maxProperties = {0}: ممکن است فقط حداکثر {1} ویژگی داشته باشد
+maximum = {0}: باید حداکثر مقدار {1} داشته باشد
+minItems = {0}: باید حداقل {1} مورد در آرایه وجود داشته باشد
+minLength = {0}: باید حداقل {1} کاراکتر باشد
+minProperties = {0}: should have a minimum of {1} properties
+minimum = {0}: باید حداقل {1} ویژگی داشته باشد
+multipleOf = {0}: باید مضرب از {1} باشد
+not = {0}: نباید برای طرحواره معتبر باشد {1}
+notAllowed = {0}.{1}: مجاز نیست اما در داده ها وجود دارد
+oneOf = {0}: باید برای یک و تنها یکی از طرحواره ها معتبر باشد، اما بیش از یکی معتبر است: {1}
+pattern = {0}: با الگوی regex مطابقت ندارد {1}
+patternProperties = {0}: دارای مقداری خطا با 'خواص الگو'
+prefixItems = {0}[{1}]: هیچ اعتبارسنجی در این فهرست یافت نشد
+properties = {0}: دارای خطا با 'خواص'
+propertyNames = نام مشخصه {0} برای اعتبارسنجی معتبر نیست: {1}
+readOnly = {0}: یک فیلد فقط خواندنی است، نمی توان آن را تغییر داد
+required = {0}.{1}: وجود ندارد اما لازم است
+type = {0}: {1} یافت شد، {2} مورد انتظار بود
+unevaluatedProperties = ویژگی های ارزیابی نشده در مسیرهای زیر وجود دارد {0}
+unionType = {0}: {1} پیدا شد، اما {2} مورد نیاز است
+uniqueItems = {0}: موارد موجود در آرایه باید منحصر به فرد باشند
+uuid = {0}: {1} یک {2} نامعتبر است


Test patch:
diff --git a/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java
new file mode 100644
index 0000000..da8b546
--- /dev/null
+++ b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java
@@ -0,0 +1,52 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.SpecVersion.VersionFlag;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class OverwritingCustomMessageBugTest {
+  private JsonSchema getJsonSchemaFromStreamContentV7(InputStream schemaContent) {
+    JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V7);
+    return factory.getSchema(schemaContent);
+  }
+
+  private JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+    ObjectMapper mapper = new ObjectMapper();
+    return mapper.readTree(content);
+  }
+
+  @Test
+  public void customMessageIsNotOverwritten() throws Exception {
+    Set<ValidationMessage> errors = validate();
+    Map<String, String> errorMsgMap = transferErrorMsg(errors);
+    Assertions.assertTrue(errorMsgMap.containsKey("$.toplevel[1].foos"), "error message must contains key: $.foos");
+    Assertions.assertTrue(errorMsgMap.containsKey("$.toplevel[1].bars"), "error message must contains key: $.bars");
+    Assertions.assertEquals("{0}: Must be a string with the a shape foofoofoofoo... with at least one foo", errorMsgMap.get("$.toplevel[1].foos"));
+    Assertions.assertEquals("{0}: Must be a string with the a shape barbarbar... with at least one bar", errorMsgMap.get("$.toplevel[1].bars"));
+  }
+
+
+  private Set<ValidationMessage> validate() throws Exception {
+    String schemaPath = "/schema/OverwritingCustomMessageBug.json";
+    String dataPath = "/data/OverwritingCustomMessageBug.json";
+    InputStream schemaInputStream = OverwritingCustomMessageBugTest.class.getResourceAsStream(schemaPath);
+    JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream);
+    InputStream dataInputStream = OverwritingCustomMessageBugTest.class.getResourceAsStream(dataPath);
+    JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+    return schema.validate(node);
+  }
+
+  private Map<String, String> transferErrorMsg(Set<ValidationMessage> validationMessages) {
+    Map<String, String> pathToMessage = new HashMap<>();
+    validationMessages.forEach(msg -> {
+      pathToMessage.put(msg.getPath(), msg.getMessage());
+    });
+    return pathToMessage;
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/data/OverwritingCustomMessageBug.json b/src/test/resources/data/OverwritingCustomMessageBug.json
new file mode 100644
index 0000000..4866053
--- /dev/null
+++ b/src/test/resources/data/OverwritingCustomMessageBug.json
@@ -0,0 +1,19 @@
+{
+  "toplevel": [
+    {
+      "foos": "foo",
+      "Nope": "a",
+      "bars": "bar"
+    },
+    {
+      "foos": "fee",
+      "Nope": "b",
+      "bars": "baz"
+    },
+    {
+      "foos": "foo",
+      "Nope": "c",
+      "bars": "bar"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/schema/OverwritingCustomMessageBug.json b/src/test/resources/schema/OverwritingCustomMessageBug.json
new file mode 100644
index 0000000..6ead21b
--- /dev/null
+++ b/src/test/resources/schema/OverwritingCustomMessageBug.json
@@ -0,0 +1,31 @@
+{
+  "type": "object",
+  "properties": {
+    "toplevel": {
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "foos": {
+            "type": "string",
+            "pattern": "(foo)+",
+            "message": {
+              "pattern": "{0}: Must be a string with the a shape foofoofoofoo... with at least one foo"
+            }
+          },
+          "Nope": {
+            "type": "string"
+          },
+          "bars": {
+            "type": "string",
+            "pattern": "(bar)+",
+            "message": {
+              "pattern": "{0}: Must be a string with the a shape barbarbar... with at least one bar"
+            }
+          }
+        }
+      },
+      "minItems": 1
+    }
+  }
+}
\ No newline at end of file