Repository: networknt/json-schema-validator
Pull request #438 (2021-08-20 12:37:42)

"Adding custom message support"
We can create custom message, by adding another property in schema i.e **message**  for e.g.

If our schema is like this:

```
"schema": {
      "properties": {
        "foo": {
          "type": "integer"
        },
        "bar": {
          "type": "string"
        }
      }
    },
```

Then to provide custom message, we have to modify it to:

```
"schema": {
      "properties": {
        "foo": {
          "type": "integer"
        },
        "bar": {
          "type": "string"
        }
      },
    **"message": {
        "type" : "Invalid type"
      }**
    },
```


Since we are getting arguments in ValidationMessage, we can make this message more illustrative using dynamic arguments.
e.g.  **{0} invalid type** where , {0}  will be path


Patch:
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index 0219e39..f1312bf 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -70,7 +70,10 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
                                 registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at);
                                 // now we have to check whether we have hit the right target
                                 final String discriminatorPropertyName = discriminator.get("propertyName").asText();
-                                final String discriminatorPropertyValue = node.get(discriminatorPropertyName).textValue();
+                                final JsonNode discriminatorNode = node.get(discriminatorPropertyName);
+                                final String discriminatorPropertyValue = discriminatorNode == null
+                                        ? null
+                                        : discriminatorNode.textValue();
 
                                 final JsonSchema jsonSchema = parentSchema;
                                 checkDiscriminatorMatch(
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index 72f7a6b..5b1a471 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -65,7 +65,7 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
                 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 must to call TypeValidator
+                    //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;
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 81f4317..9ad401b 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -195,6 +195,11 @@ public abstract class BaseJsonValidator implements JsonValidator {
                                                   final ObjectNode discriminator,
                                                   final String discriminatorPropertyValue,
                                                   final JsonSchema jsonSchema) {
+        if (discriminatorPropertyValue == null) {
+            currentDiscriminatorContext.markMatch();
+            return;
+        }
+
         final JsonNode discriminatorMapping = discriminator.get("mapping");
         if (null == discriminatorMapping) {
             checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext,
diff --git a/src/main/java/com/networknt/schema/ErrorMessageType.java b/src/main/java/com/networknt/schema/ErrorMessageType.java
index 226cdc5..9d3b9b2 100644
--- a/src/main/java/com/networknt/schema/ErrorMessageType.java
+++ b/src/main/java/com/networknt/schema/ErrorMessageType.java
@@ -35,4 +35,8 @@ public interface ErrorMessageType {
      */
     MessageFormat getMessageFormat();
 
+    default String getCustomMessage() {
+        return null;
+    }
+
 }
diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java
index f17d0e6..5c77935 100644
--- a/src/main/java/com/networknt/schema/FormatKeyword.java
+++ b/src/main/java/com/networknt/schema/FormatKeyword.java
@@ -68,4 +68,11 @@ public class FormatKeyword implements Keyword {
     public String getValue() {
         return type.getValue();
     }
+
+    @Override
+    public void setCustomMessage(String message) {
+        type.setCustomMessage(message);
+    }
+
+
 }
diff --git a/src/main/java/com/networknt/schema/I18nSupport.java b/src/main/java/com/networknt/schema/I18nSupport.java
new file mode 100644
index 0000000..0f22352
--- /dev/null
+++ b/src/main/java/com/networknt/schema/I18nSupport.java
@@ -0,0 +1,17 @@
+package com.networknt.schema;
+
+import java.util.ResourceBundle;
+
+/**
+ * Created by leaves chen leaves615@gmail.com on 2021/8/23.
+ *
+ * @Author leaves chen leaves615@gmail.com
+ */
+public class I18nSupport {
+    private static final String BASE_NAME = "jsv-messages";
+    private static ResourceBundle bundle = ResourceBundle.getBundle(BASE_NAME);
+
+    public static String getString(String key) {
+        return bundle.getString(key);
+    }
+}
diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java
index 0a87929..1f3a356 100644
--- a/src/main/java/com/networknt/schema/JsonMetaSchema.java
+++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java
@@ -181,7 +181,8 @@ public class JsonMetaSchema {
                             new NonValidationKeyword("$comment"),
                             new NonValidationKeyword("contentMediaType"),
                             new NonValidationKeyword("contentEncoding"),
-                            new NonValidationKeyword("examples")
+                            new NonValidationKeyword("examples"),
+                            new NonValidationKeyword("message")
                     ))
                     .build();
         }
@@ -375,8 +376,9 @@ public class JsonMetaSchema {
         return idKeyword;
     }
 
+
     public JsonValidator newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode,
-                                      JsonSchema parentSchema) {
+                                      JsonSchema parentSchema, String customMessage) {
 
         try {
             Keyword kw = keywords.get(keyword);
@@ -386,6 +388,7 @@ public class JsonMetaSchema {
                 }
                 return null;
             }
+            kw.setCustomMessage(customMessage);
             return kw.newValidator(schemaPath, schemaNode, parentSchema, validationContext);
         } catch (InvocationTargetException e) {
             if (e.getTargetException() instanceof JsonSchemaException) {
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 1050c95..18936b9 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -208,10 +208,12 @@ public class JsonSchema extends BaseJsonValidator {
         Map<String, JsonValidator> validators = new HashMap<String, JsonValidator>();
         if (schemaNode.isBoolean()) {
             if (schemaNode.booleanValue()) {
-                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "true", schemaNode, this);
+                final String customMessage = getCustomMessage(schemaNode, "true");
+                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "true", schemaNode, this, customMessage);
                 validators.put(getSchemaPath() + "/true", validator);
             } else {
-                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "false", schemaNode, this);
+                final String customMessage = getCustomMessage(schemaNode, "false");
+                JsonValidator validator = validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage);
                 validators.put(getSchemaPath() + "/false", validator);
             }
         } else {
@@ -219,8 +221,8 @@ public class JsonSchema extends BaseJsonValidator {
             while (pnames.hasNext()) {
                 String pname = pnames.next();
                 JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname);
-
-                JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this);
+                String customMessage = getCustomMessage(schemaNode, pname);
+                JsonValidator validator = validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage);
                 if (validator != null) {
                     validators.put(getSchemaPath() + "/" + pname, validator);
 
@@ -234,6 +236,24 @@ public class JsonSchema extends BaseJsonValidator {
         return validators;
     }
 
+    private String getCustomMessage(JsonNode schemaNode, String pname) {
+        final JsonSchema parentSchema = getParentSchema();
+        final JsonNode message = getMessageNode(schemaNode, parentSchema);
+        if(message != null && message.get(pname) != null) {
+            return message.get(pname).asText();
+        }
+        return null;
+    }
+
+    private JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema) {
+        JsonNode nodeContainingMessage;
+        if (parentSchema == null)
+            nodeContainingMessage  = schemaNode;
+        else
+            nodeContainingMessage = parentSchema.schemaNode;
+        return nodeContainingMessage.get("message");
+    }
+
     /************************ START OF VALIDATE METHODS **********************************/
 
     public Set<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, String at) {
@@ -264,7 +284,8 @@ public class JsonSchema extends BaseJsonValidator {
                     }
 
                     final String discriminatorPropertyName = discriminatorToUse.get("propertyName").asText();
-                    final String discriminatorPropertyValue = jsonNode.get(discriminatorPropertyName).asText();
+                    final JsonNode discriminatorNode = jsonNode.get(discriminatorPropertyName);
+                    final String discriminatorPropertyValue = discriminatorNode == null ? null : discriminatorNode.asText();
                     checkDiscriminatorMatch(discriminatorContext,
                                             discriminatorToUse,
                                             discriminatorPropertyValue,
diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java
index 66bf35b..0f9283b 100644
--- a/src/main/java/com/networknt/schema/Keyword.java
+++ b/src/main/java/com/networknt/schema/Keyword.java
@@ -22,5 +22,9 @@ import com.fasterxml.jackson.databind.JsonNode;
 public interface Keyword {
     String getValue();
 
+    default void setCustomMessage(String message) {
+        //setCustom message
+    }
+
     JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception;
 }
diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java
index 5efcc65..a7d2474 100644
--- a/src/main/java/com/networknt/schema/ValidationContext.java
+++ b/src/main/java/com/networknt/schema/ValidationContext.java
@@ -53,8 +53,8 @@ public class ValidationContext {
     }
 
     public JsonValidator newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode,
-                                      JsonSchema parentSchema) {
-        return metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema);
+                                      JsonSchema parentSchema, String customMessage) {
+        return metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema, customMessage);
     }
 
     public String resolveSchemaId(JsonNode schemaNode) {
diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java
index b0bc1b6..b68ec56 100644
--- a/src/main/java/com/networknt/schema/ValidationMessage.java
+++ b/src/main/java/com/networknt/schema/ValidationMessage.java
@@ -16,6 +16,8 @@
 
 package com.networknt.schema;
 
+import org.apache.commons.lang3.StringUtils;
+
 import java.text.MessageFormat;
 import java.util.Arrays;
 import java.util.Map;
@@ -114,7 +116,8 @@ public class ValidationMessage {
     public static ValidationMessage of(String type, ErrorMessageType errorMessageType, String at, String... arguments) {
         ValidationMessage.Builder builder = new ValidationMessage.Builder();
         builder.code(errorMessageType.getErrorCode()).path(at).arguments(arguments)
-                .format(errorMessageType.getMessageFormat()).type(type);
+                .format(errorMessageType.getMessageFormat()).type(type)
+                .customMessage(errorMessageType.getCustomMessage());
         return builder.build();
     }
 
@@ -132,6 +135,7 @@ public class ValidationMessage {
         private String[] arguments;
         private Map<String, Object> details;
         private MessageFormat format;
+        private String customMessage;
 
         public Builder type(String type) {
             this.type = type;
@@ -163,6 +167,11 @@ public class ValidationMessage {
             return this;
         }
 
+        public Builder customMessage(String customMessage) {
+            this.customMessage = customMessage;
+            return this;
+        }
+
         public ValidationMessage build() {
             ValidationMessage msg = new ValidationMessage();
             msg.setType(type);
@@ -179,7 +188,12 @@ public class ValidationMessage {
                         objs[i] = arguments[i - 1];
                     }
                 }
-                msg.setMessage(format.format(objs));
+                if(StringUtils.isNotBlank(customMessage)) {
+                    msg.setMessage(customMessage);
+                } else {
+                    msg.setMessage(format.format(objs));
+                }
+
             }
 
             return msg;
diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
index 1a9c313..3e77723 100644
--- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java
+++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java
@@ -26,54 +26,53 @@ import java.util.List;
 import java.util.Map;
 
 public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
-    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(
-            "{0}.{1}: is not defined in the schema and the schema does not allow additional properties"), AdditionalPropertiesValidator.class, 15), // v4|v6|v7|v201909
-    ALL_OF("allOf", "1002", new MessageFormat("{0}: should be valid to all the schemas {1}"), AllOfValidator.class, 15),
-    ANY_OF("anyOf", "1003", new MessageFormat("{0}: should be valid to any of the schemas {1}"), AnyOfValidator.class, 15),
-    CROSS_EDITS("crossEdits", "1004", new MessageFormat("{0}: has an error with 'cross edits'"), null, 15),
-    DEPENDENCIES("dependencies", "1007", new MessageFormat("{0}: has an error with dependencies {1}"), DependenciesValidator.class, 15),
-    EDITS("edits", "1005", new MessageFormat("{0}: has an error with 'edits'"), null, 15),
-    ENUM("enum", "1008", new MessageFormat("{0}: does not have a value in the enumeration {1}"), EnumValidator.class, 15),
-    FORMAT("format", "1009", new MessageFormat("{0}: does not match the {1} pattern {2}"), null, 15) {
+    ADDITIONAL_PROPERTIES("additionalProperties", "1001", new MessageFormat(I18nSupport.getString("additionalProperties")), AdditionalPropertiesValidator.class, 15), // v4|v6|v7|v201909
+    ALL_OF("allOf", "1002", new MessageFormat(I18nSupport.getString("allOf")), AllOfValidator.class, 15),
+    ANY_OF("anyOf", "1003", new MessageFormat(I18nSupport.getString("anyOf")), AnyOfValidator.class, 15),
+    CROSS_EDITS("crossEdits", "1004", new MessageFormat(I18nSupport.getString("crossEdits")), null, 15),
+    DEPENDENCIES("dependencies", "1007", new MessageFormat(I18nSupport.getString("dependencies")), DependenciesValidator.class, 15),
+    EDITS("edits", "1005", new MessageFormat(I18nSupport.getString("edits")), null, 15),
+    ENUM("enum", "1008", new MessageFormat(I18nSupport.getString("enum")), EnumValidator.class, 15),
+    FORMAT("format", "1009", new MessageFormat(I18nSupport.getString("format")), null, 15) {
         @Override
         public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
                 throws Exception {
             throw new UnsupportedOperationException("Use FormatKeyword instead");
         }
     },
-    ITEMS("items", "1010", new MessageFormat("{0}[{1}]: no validator found at this index"), ItemsValidator.class, 15),
-    MAXIMUM("maximum", "1011", new MessageFormat("{0}: must have a maximum value of {1}"), MaximumValidator.class, 15),
-    MAX_ITEMS("maxItems", "1012", new MessageFormat("{0}: there must be a maximum of {1} items in the array"), MaxItemsValidator.class, 15),
-    MAX_LENGTH("maxLength", "1013", new MessageFormat("{0}: may only be {1} characters long"), MaxLengthValidator.class, 15),
-    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat("{0}: may only have a maximum of {1} properties"), MaxPropertiesValidator.class, 15),
-    MINIMUM("minimum", "1015", new MessageFormat("{0}: must have a minimum value of {1}"), MinimumValidator.class, 15),
-    MIN_ITEMS("minItems", "1016", new MessageFormat("{0}: there must be a minimum of {1} items in the array"), MinItemsValidator.class, 15),
-    MIN_LENGTH("minLength", "1017", new MessageFormat("{0}: must be at least {1} characters long"), MinLengthValidator.class, 15),
-    MIN_PROPERTIES("minProperties", "1018", new MessageFormat("{0}: should have a minimum of {1} properties"), MinPropertiesValidator.class, 15),
-    MULTIPLE_OF("multipleOf", "1019", new MessageFormat("{0}: must be multiple of {1}"), MultipleOfValidator.class, 15),
-    NOT_ALLOWED("notAllowed", "1033", new MessageFormat("{0}.{1}: is not allowed but it is in the data"), NotAllowedValidator.class, 15),
-    NOT("not", "1020", new MessageFormat("{0}: should not be valid to the schema {1}"), NotValidator.class, 15),
-    ONE_OF("oneOf", "1022", new MessageFormat("{0}: should be valid to one and only one of the schemas {1}"), OneOfValidator.class, 15),
-    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat("{0}: has some error with 'pattern properties'"), PatternPropertiesValidator.class, 15),
-    PATTERN("pattern", "1023", new MessageFormat("{0}: does not match the regex pattern {1}"), PatternValidator.class, 15),
-    PROPERTIES("properties", "1025", new MessageFormat("{0}: has an error with 'properties'"), PropertiesValidator.class, 15),
-    READ_ONLY("readOnly", "1032", new MessageFormat("{0}: is a readonly field, it cannot be changed"), ReadOnlyValidator.class, 15),
-    REF("$ref", "1026", new MessageFormat("{0}: has an error with 'refs'"), RefValidator.class, 15),
-    REQUIRED("required", "1028", new MessageFormat("{0}.{1}: is missing but it is required"), RequiredValidator.class, 15),
-    TYPE("type", "1029", new MessageFormat("{0}: {1} found, {2} expected"), TypeValidator.class, 15),
-    UNION_TYPE("unionType", "1030", new MessageFormat("{0}: {1} found, but {2} is required"), UnionTypeValidator.class, 15),
-    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat("{0}: the items in the array must be unique"), UniqueItemsValidator.class, 15),
+    ITEMS("items", "1010", new MessageFormat(I18nSupport.getString("items")), ItemsValidator.class, 15),
+    MAXIMUM("maximum", "1011", new MessageFormat(I18nSupport.getString("maximum")), MaximumValidator.class, 15),
+    MAX_ITEMS("maxItems", "1012", new MessageFormat(I18nSupport.getString("maxItems")), MaxItemsValidator.class, 15),
+    MAX_LENGTH("maxLength", "1013", new MessageFormat(I18nSupport.getString("maxLength")), MaxLengthValidator.class, 15),
+    MAX_PROPERTIES("maxProperties", "1014", new MessageFormat(I18nSupport.getString("maxProperties")), MaxPropertiesValidator.class, 15),
+    MINIMUM("minimum", "1015", new MessageFormat(I18nSupport.getString("minimum")), MinimumValidator.class, 15),
+    MIN_ITEMS("minItems", "1016", new MessageFormat(I18nSupport.getString("minItems")), MinItemsValidator.class, 15),
+    MIN_LENGTH("minLength", "1017", new MessageFormat(I18nSupport.getString("minLength")), MinLengthValidator.class, 15),
+    MIN_PROPERTIES("minProperties", "1018", new MessageFormat(I18nSupport.getString("minProperties")), MinPropertiesValidator.class, 15),
+    MULTIPLE_OF("multipleOf", "1019", new MessageFormat(I18nSupport.getString("multipleOf")), MultipleOfValidator.class, 15),
+    NOT_ALLOWED("notAllowed", "1033", new MessageFormat(I18nSupport.getString("notAllowed")), NotAllowedValidator.class, 15),
+    NOT("not", "1020", new MessageFormat(I18nSupport.getString("not")), NotValidator.class, 15),
+    ONE_OF("oneOf", "1022", new MessageFormat(I18nSupport.getString("oneOf")), OneOfValidator.class, 15),
+    PATTERN_PROPERTIES("patternProperties", "1024", new MessageFormat(I18nSupport.getString("patternProperties")), PatternPropertiesValidator.class, 15),
+    PATTERN("pattern", "1023", new MessageFormat(I18nSupport.getString("pattern")), PatternValidator.class, 15),
+    PROPERTIES("properties", "1025", new MessageFormat(I18nSupport.getString("properties")), PropertiesValidator.class, 15),
+    READ_ONLY("readOnly", "1032", new MessageFormat(I18nSupport.getString("readOnly")), ReadOnlyValidator.class, 15),
+    REF("$ref", "1026", new MessageFormat(I18nSupport.getString("$ref")), RefValidator.class, 15),
+    REQUIRED("required", "1028", new MessageFormat(I18nSupport.getString("required")), RequiredValidator.class, 15),
+    TYPE("type", "1029", new MessageFormat(I18nSupport.getString("type")), TypeValidator.class, 15),
+    UNION_TYPE("unionType", "1030", new MessageFormat(I18nSupport.getString("unionType")), UnionTypeValidator.class, 15),
+    UNIQUE_ITEMS("uniqueItems", "1031", new MessageFormat(I18nSupport.getString("uniqueItems")), UniqueItemsValidator.class, 15),
     DATETIME("date-time", "1034", new MessageFormat("{0}: {1} is an invalid {2}"), null, 15),
-    UUID("uuid", "1035", new MessageFormat("{0}: {1} is an invalid {2}"), null, 15),
-    ID("id", "1036", new MessageFormat("{0}: {1} is an invalid segment for URI {2}"), null, 15),
+    UUID("uuid", "1035", new MessageFormat(I18nSupport.getString("uuid")), null, 15),
+    ID("id", "1036", new MessageFormat(I18nSupport.getString("id")), null, 15),
     IF_THEN_ELSE("if", "1037", null, IfValidator.class, 12),  // V7|V201909
-    EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", new MessageFormat("{0}: must have a exclusive maximum value of {1}"), ExclusiveMaximumValidator.class, 14),  // V6|V7|V201909
-    EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", new MessageFormat("{0}: must have a exclusive minimum value of {1}"), ExclusiveMinimumValidator.class, 14),
+    EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", new MessageFormat(I18nSupport.getString("exclusiveMaximum")), ExclusiveMaximumValidator.class, 14),  // V6|V7|V201909
+    EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", new MessageFormat(I18nSupport.getString("exclusiveMinimum")), ExclusiveMinimumValidator.class, 14),
     TRUE("true", "1040", null, TrueValidator.class, 14),
-    FALSE("false", "1041", new MessageFormat("Boolean schema false is not valid"), FalseValidator.class, 14),
-    CONST("const", "1042", new MessageFormat("{0}: must be a constant value {1}"), ConstValidator.class, 14),
-    CONTAINS("contains", "1043", new MessageFormat("{0}: does not contain an element that passes these validations: {1}"), ContainsValidator.class, 14),
-    PROPERTYNAMES("propertyNames", "1044", new MessageFormat("Property name {0} is not valid for validation: {1}"), PropertyNamesValidator.class, 14);
+    FALSE("false", "1041", new MessageFormat(I18nSupport.getString("false")), FalseValidator.class, 14),
+    CONST("const", "1042", new MessageFormat(I18nSupport.getString("const")), ConstValidator.class, 14),
+    CONTAINS("contains", "1043", new MessageFormat(I18nSupport.getString("contains")), ContainsValidator.class, 14),
+    PROPERTYNAMES("propertyNames", "1044", new MessageFormat(I18nSupport.getString("propertyNames")), PropertyNamesValidator.class, 14);
 
     private static Map<String, ValidatorTypeCode> constants = new HashMap<String, ValidatorTypeCode>();
     private static SpecVersion specVersion = new SpecVersion();
@@ -87,6 +86,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
     private final String value;
     private final String errorCode;
     private final MessageFormat messageFormat;
+    private String customMessage;
     private final String errorCodeKey;
     private final Class validator;
     private final long versionCode;
@@ -99,6 +99,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
         this.errorCodeKey = value + "ErrorCode";
         this.validator = validator;
         this.versionCode = versionCode;
+        this.customMessage = null;
     }
 
     public static List<ValidatorTypeCode> getNonFormatKeywords(SpecVersion.VersionFlag versionFlag) {
@@ -148,6 +149,14 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType {
         return messageFormat;
     }
 
+    public void setCustomMessage(String message) {
+        this.customMessage = message;
+    }
+
+    public String getCustomMessage() {
+        return customMessage;
+    }
+
     public String getErrorCodeKey() {
         return errorCodeKey;
     }
diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties
new file mode 100644
index 0000000..e2352a0
--- /dev/null
+++ b/src/main/resources/jsv-messages.properties
@@ -0,0 +1,38 @@
+additionalProperties = {0}.{1}: is not defined in the schema and the schema does not allow additional properties
+allOf = {0}: should be valid to all the schemas {1}
+anyOf = {0}: should be valid to any of the schemas {1}
+crossEdits = {0}: has an error with 'cross edits'
+dependencies = {0}: has an error with dependencies {1}
+edits = {0}: has an error with 'edits'
+enum = {0}: does not have a value in the enumeration {1}
+format = {0}: does not match the {1} pattern {2}
+items = {0}[{1}]: no validator found at this index
+maximum = {0}: must have a maximum value of {1}
+maxItems = {0}: there must be a maximum of {1} items in the array
+maxLength = {0}: may only be {1} characters long
+maxProperties = {0}: may only have a maximum of {1} properties
+minimum = {0}: must have a minimum value of {1}
+minItems = {0}: there must be a minimum of {1} items in the array
+minLength = {0}: must be at least {1} characters long
+minProperties = {0}: should have a minimum of {1} properties
+multipleOf = {0}: must be multiple of {1}
+notAllowed = {0}.{1}: is not allowed but it is in the data
+not = {0}: should not be valid to the schema {1}
+oneOf = {0}: should be valid to one and only one of the schemas {1}
+patternProperties = {0}: has some error with 'pattern properties'
+pattern = {0}: does not match the regex pattern {1}
+properties = {0}: has an error with 'properties'
+readOnly = {0}: is a readonly field, it cannot be changed
+$ref = {0}: has an error with 'refs'
+required = {0}.{1}: is missing but it is required
+type = {0}: {1} found, {2} expected
+unionType = {0}: {1} found, but {2} is required
+uniqueItems = {0}: the items in the array must be unique
+uuid = {0}: {1} is an invalid {2}
+id = {0}: {1} is an invalid segment for URI {2}
+exclusiveMaximum = {0}: must have a exclusive maximum value of {1}
+exclusiveMinimum = {0}: must have a exclusive minimum value of {1}
+false = Boolean schema false is not valid
+const = {0}: must be a constant value {1}
+contains = {0}: does not contain an element that passes these validations: {1}
+propertyNames = Property name {0} is not valid for validation: {1}
diff --git a/src/main/resources/jsv-messages_zh_cn.properties b/src/main/resources/jsv-messages_zh_cn.properties
new file mode 100644
index 0000000..1dde79c
--- /dev/null
+++ b/src/main/resources/jsv-messages_zh_cn.properties
@@ -0,0 +1,38 @@
+additionalProperties = {0}.{1}\uFF1A\u672A\u5728\u67B6\u6784\u4E2D\u5B9A\u4E49\u4E14\u67B6\u6784\u4E0D\u5141\u8BB8\u9644\u52A0\u5C5E\u6027
+allOf = {0}\uFF1A\u5E94\u8BE5\u5BF9\u6240\u6709\u6A21\u5F0F {1} \u90FD\u6709\u6548
+anyOf = {0}\uFF1A\u5E94\u8BE5\u5BF9\u4EFB\u4F55\u67B6\u6784 {1} \u90FD\u6709\u6548
+crossEdits = {0}\uFF1A\u201C\u4EA4\u53C9\u7F16\u8F91\u201D\u6709\u9519\u8BEF
+dependencies = {0}\uFF1A\u4F9D\u8D56\u9879 {1} \u6709\u9519\u8BEF
+edits = {0}\uFF1A\u201C\u7F16\u8F91\u201D\u6709\u9519\u8BEF
+enum = {0}\uFF1A\u679A\u4E3E {1} \u4E2D\u6CA1\u6709\u503C
+format = {0}\uFF1A\u4E0E {1} \u6A21\u5F0F {2} \u4E0D\u5339\u914D
+items = {0}[{1}]\uFF1A\u5728\u6B64\u7D22\u5F15\u4E2D\u627E\u4E0D\u5230\u9A8C\u8BC1\u5668
+maximum = {0}\uFF1A\u6700\u5927\u503C\u5FC5\u987B\u4E3A {1}
+maxItems = {0}\uFF1A\u6570\u7EC4\u4E2D\u6700\u591A\u5FC5\u987B\u6709 {1} \u4E2A\u9879\u76EE
+maxLength = {0}\uFF1A\u53EF\u80FD\u53EA\u6709 {1} \u4E2A\u5B57\u7B26\u957F
+maxProperties = {0}\uFF1A\u6700\u591A\u53EA\u80FD\u6709 {1} \u4E2A\u5C5E\u6027
+minimum = {0}\uFF1A\u6700\u5C0F\u503C\u5FC5\u987B\u4E3A {1}
+minItems = {0}\uFF1A\u6570\u7EC4\u4E2D\u5FC5\u987B\u81F3\u5C11\u6709 {1} \u4E2A\u9879\u76EE
+minLength = {0}\uFF1A\u5FC5\u987B\u81F3\u5C11\u4E3A {1} \u4E2A\u5B57\u7B26\u957F
+minProperties = {0}\uFF1A\u5E94\u8BE5\u81F3\u5C11\u6709 {1} \u4E2A\u5C5E\u6027
+multipleOf = {0}\uFF1A\u5FC5\u987B\u662F {1} \u7684\u500D\u6570
+notAllowed = {0}.{1}\uFF1A\u4E0D\u88AB\u5141\u8BB8\u4F46\u5728\u6570\u636E\u4E2D
+not = {0}\uFF1A\u4E0D\u5E94\u5BF9\u67B6\u6784 {1} \u6709\u6548
+oneOf = {0}\uFF1A\u5E94\u8BE5\u5BF9\u4E00\u79CD\u4E14\u4EC5\u4E00\u79CD\u6A21\u5F0F\u6709\u6548 {1}
+patternProperties = {0}\uFF1A\u201C\u6A21\u5F0F\u5C5E\u6027\u201D\u6709\u4E00\u4E9B\u9519\u8BEF
+pattern = {0}\uFF1A\u4E0E\u6B63\u5219\u8868\u8FBE\u5F0F\u6A21\u5F0F {1} \u4E0D\u5339\u914D
+properties = {0}\uFF1A\u201C\u5C5E\u6027\u201D\u6709\u9519\u8BEF
+readOnly = {0}\uFF1A\u662F\u53EA\u8BFB\u5B57\u6BB5\uFF0C\u4E0D\u80FD\u66F4\u6539
+$ref = {0}\uFF1A'refs' \u6709\u9519\u8BEF
+required = {0}.{1}\uFF1A\u7F3A\u5C11\u4F46\u5B83\u662F\u5FC5\u9700\u7684
+type = {0}\uFF1A\u627E\u5230 {1}\uFF0C\u9884\u671F\u4E3A {2}
+unionType = {0}\uFF1A\u627E\u5230 {1}\uFF0C\u4F46\u9700\u8981 {2}
+uniqueItems = {0}\uFF1A\u6570\u7EC4\u4E2D\u7684\u9879\u76EE\u5FC5\u987B\u662F\u552F\u4E00\u7684
+uuid = {0}\uFF1A{1} \u662F\u65E0\u6548\u7684 {2}
+id = {0}\uFF1A{1} \u662F URI {2} \u7684\u65E0\u6548\u6BB5
+exclusiveMaximum = {0}\uFF1A\u5FC5\u987B\u5177\u6709 {1} \u7684\u72EC\u5360\u6700\u5927\u503C
+exclusiveMinimum = {0}\uFF1A\u5FC5\u987B\u5177\u6709 {1} \u7684\u552F\u4E00\u6700\u5C0F\u503C
+false = \u5E03\u5C14\u67B6\u6784 false \u65E0\u6548
+const = {0}\uFF1A\u5FC5\u987B\u662F\u5E38\u91CF\u503C {1}
+contains = {0}\uFF1A\u4E0D\u5305\u542B\u901A\u8FC7\u8FD9\u4E9B\u9A8C\u8BC1\u7684\u5143\u7D20\uFF1A{1}
+propertyNames = \u5C5E\u6027\u540D\u79F0 {0} \u5BF9\u9A8C\u8BC1\u65E0\u6548\uFF1A{1}


Test patch:
diff --git a/src/test/java/com/networknt/schema/Issue426Test.java b/src/test/java/com/networknt/schema/Issue426Test.java
new file mode 100644
index 0000000..5c9b8ac
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue426Test.java
@@ -0,0 +1,42 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.Set;
+
+/**
+ * Validating custom message
+ */
+public class Issue426Test {
+    protected JsonSchema getJsonSchemaFromStreamContentV7(InputStream schemaContent) {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        return factory.getSchema(schemaContent);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readTree(content);
+    }
+
+    @Test
+    public void shouldWorkV7() throws Exception {
+        String schemaPath = "/schema/issue426-v7.json";
+        String dataPath = "/data/issue426.json";
+        InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath);
+        JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream);
+        InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assert.assertEquals(2, errors.size());
+        final JsonNode message = schema.schemaNode.get("message");
+        for(ValidationMessage error : errors) {
+            //validating custom message
+            Assert.assertEquals(message.get(error.getType()).asText(),  error.getMessage());
+        }
+    }
+}
+
diff --git a/src/test/resources/data/issue426.json b/src/test/resources/data/issue426.json
new file mode 100644
index 0000000..a1d6307
--- /dev/null
+++ b/src/test/resources/data/issue426.json
@@ -0,0 +1,9 @@
+{
+  "firstName": {},
+  "foo": [
+    1,
+    2,
+    3,
+    4
+  ]
+}
diff --git a/src/test/resources/openapi3/discriminator.json b/src/test/resources/openapi3/discriminator.json
index 2e32b70..6d55ece 100644
--- a/src/test/resources/openapi3/discriminator.json
+++ b/src/test/resources/openapi3/discriminator.json
@@ -277,6 +277,75 @@
                     "@type": "Bathroom"
                 },
                 "valid": false
+            },
+            {
+                "description": "discriminator missing - falling back to default validation with failure as @type is mandatory",
+                "data": {
+                },
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "absent discriminator value as discriminator marked as non-mandatory (which is against OAS3 recommendations)",
+        "schema": {
+            "anyOf": [
+                {
+                    "$ref": "#/components/schemas/Room"
+                },
+                {
+                    "$ref": "#/components/schemas/BedRoom"
+                }
+            ],
+            "components": {
+                "schemas": {
+                    "Room": {
+                        "type": "object",
+                        "properties": {
+                            "intOrStringType": {
+                                "type": "integer"
+                            }
+                        },
+                        "discriminator": {
+                            "propertyName": "@type",
+                            "mapping": {
+                                "bed": "#/components/schemas/BedRoom"
+                            }
+                        }
+                    },
+                    "BedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "intOrStringType": {
+                                        "type": "string"
+                                    }
+                                }
+                            }
+                        ]
+                    }
+                }
+            }
+        },
+        "tests": [
+            {
+                "description": "discriminator missing - falling back to default anyOf behavior",
+                "data": {
+                    "intOrStringType": 4
+                },
+                "valid": true
+            },
+            {
+                "description": "discriminator missing - with constructed schema conflict",
+                "data": {
+                    "intOrStringType": "cannot match due to allOf"
+                },
+                "valid": false
             }
         ]
     },
diff --git a/src/test/resources/schema/issue426-v7.json b/src/test/resources/schema/issue426-v7.json
new file mode 100644
index 0000000..9b01e2b
--- /dev/null
+++ b/src/test/resources/schema/issue426-v7.json
@@ -0,0 +1,20 @@
+{
+  "$id": "https://example.com/person.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Person",
+  "type": "object",
+  "properties": {
+    "firstName": {
+      "type": "string",
+      "description": "The person's first name."
+    },
+    "foo": {
+      "type": "array",
+      "maxItems": 3
+    }
+  },
+  "message": {
+    "maxItems" : "MaxItem must be 3 only",
+    "type" : "Invalid type"
+  }
+}