diff --git a/doc/cust-msg.md b/doc/cust-msg.md index d5863f938..c6c96137b 100644 --- a/doc/cust-msg.md +++ b/doc/cust-msg.md @@ -6,7 +6,7 @@ The json schema itself has a place for the customised message. ## Examples ### Example 1 : The custom message can be provided outside properties for each type, as shown in the schema below. -````json +```json { "type": "object", "properties": { @@ -24,10 +24,10 @@ The custom message can be provided outside properties for each type, as shown in "type" : "Invalid type" } } -```` +``` ### Example 2 : To keep custom messages distinct for each type, one can even give them in each property. -````json +```json { "type": "object", "properties": { @@ -47,14 +47,62 @@ To keep custom messages distinct for each type, one can even give them in each p } } } -```` +``` +### Example 3 : +For the keywords `required` and `dependencies`, different messages can be specified for different properties. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "'foo' is required", + "bar" : "'bar' is required" + } + } +} +``` +### Example 4 : +The message can use arguments but note that single quotes need to be escaped as `java.text.MessageFormat` will be used to format the message. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "{0}: ''foo'' is required", + "bar" : "{0}: ''bar'' is required" + } + } +} +``` ## Format -````json +```json "message": { [validationType] : [customMessage] } -```` +``` Users can express custom message in the **'message'** field. The **'validation type'** should be the key and the **'custom message'** should be the value. diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 69660b659..42b489095 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -97,7 +97,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!allowedProperties.contains(pname) && !handledByPatternProperties) { if (!allowAdditionalProperties) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), pname)); + errors.add(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), pname)); } else { if (additionalPropertiesSchema != null) { ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index ca63ebfcc..bffac5e13 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -77,8 +77,8 @@ public Set validate(ExecutionContext executionContext, JsonNo //If schema has type validator and node type doesn't match with schemaType then ignore it //For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typeValidator.getSchemaType().toString())); + allErrors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typeValidator.getSchemaType().toString())); continue; } } @@ -107,8 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); + allErrors.add(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); } else { // Clear all errors. allErrors.clear(); @@ -135,8 +135,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { final Set errors = new HashSet<>(); - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); return Collections.unmodifiableSet(errors); } } finally { diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 4c3aea4cf..df47af289 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -36,12 +36,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } } else if (!schemaNode.equals(node)) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index abbae9818..5a147c914 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -112,6 +112,6 @@ public void preloadJsonSchema() { } private Set boundsViolated(String messageKey, Locale locale, String at, int bounds) { - return Collections.singleton(buildValidationMessage(at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); + return Collections.singleton(buildValidationMessage(null, at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); } } diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 379e1fc10..93b1b007f 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -62,8 +62,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - propertyDeps.toString())); + errors.add(buildValidationMessage(pname, at, + executionContext.getExecutionConfig().getLocale(), propertyDeps.toString())); } } } diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index 9aa6beb9f..bf3f50042 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -56,8 +56,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (dependencies != null && !dependencies.isEmpty()) { for (String field : dependencies) { if (node.get(field) == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), field, - pname)); + errors.add(buildValidationMessage(pname, at, executionContext.getExecutionConfig().getLocale(), + field, pname)); } } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index a8b03d832..1f10ef451 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -82,7 +82,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue()); if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), error)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), error)); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ErrorMessageType.java b/src/main/java/com/networknt/schema/ErrorMessageType.java index 6be1e14f0..b7751cfd4 100644 --- a/src/main/java/com/networknt/schema/ErrorMessageType.java +++ b/src/main/java/com/networknt/schema/ErrorMessageType.java @@ -16,6 +16,8 @@ package com.networknt.schema; +import java.util.Map; + public interface ErrorMessageType { /** * Your error code. Please ensure global uniqueness. Builtin error codes are sequential numbers. @@ -26,7 +28,7 @@ public interface ErrorMessageType { */ String getErrorCode(); - default String getCustomMessage() { + default Map getCustomMessage() { return null; } diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index a667d6a22..73c285f66 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -107,8 +107,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index c1c2fda58..62a20af4a 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -114,8 +114,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 3eafa938f..10c4a9133 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -32,6 +32,6 @@ public FalseValidator(String schemaPath, final JsonNode schemaNode, JsonSchema p public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); // For the false validator, it is always not valid - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } } diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java index d38e6ff10..5074e7d37 100644 --- a/src/main/java/com/networknt/schema/FormatKeyword.java +++ b/src/main/java/com/networknt/schema/FormatKeyword.java @@ -73,7 +73,7 @@ public String getValue() { } @Override - public void setCustomMessage(String message) { + public void setCustomMessage(Map message) { this.type.setCustomMessage(message); } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index 667db95b6..49a739156 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -50,18 +50,18 @@ public Set validate(ExecutionContext executionContext, JsonNo if(format.getName().equals("ipv6")) { if(!node.textValue().trim().equals(node.textValue())) { // leading and trailing spaces - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } else if(node.textValue().contains("%")) { // zone id is not part of the ipv6 - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } } try { if (!format.matches(executionContext, node.textValue())) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - format.getName(), format.getErrorMessageDescription())); + errors.add(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 47c9fa338..9e151b4c3 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -128,7 +128,7 @@ private void doValidate(ExecutionContext executionContext, Set getKeywords() { } public JsonValidator newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, String customMessage) { + JsonSchema parentSchema, Map customMessage) { try { Keyword kw = this.keywords.get(keyword); diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index c4a2e43fb..7d9cd8ecd 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.ValidationContext.DiscriminatorContext; @@ -258,11 +259,11 @@ private Map read(JsonNode schemaNode) { Map validators = new TreeMap<>(VALIDATOR_SORT); if (schemaNode.isBoolean()) { if (schemaNode.booleanValue()) { - final String customMessage = getCustomMessage(schemaNode, "true"); + final Map customMessage = getCustomMessage(schemaNode, "true"); JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "true", schemaNode, this, customMessage); validators.put(getSchemaPath() + "/true", validator); } else { - final String customMessage = getCustomMessage(schemaNode, "false"); + final Map customMessage = getCustomMessage(schemaNode, "false"); JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage); validators.put(getSchemaPath() + "/false", validator); } @@ -276,7 +277,7 @@ private Map read(JsonNode schemaNode) { while (pnames.hasNext()) { String pname = pnames.next(); JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname); - String customMessage = getCustomMessage(schemaNode, pname); + Map customMessage = getCustomMessage(schemaNode, pname); if ("$recursiveAnchor".equals(pname)) { if (!nodeToUse.isBoolean()) { @@ -347,16 +348,29 @@ private long activeDialect() { return lhs.compareTo(rhs); // TODO: This smells. We are performing a lexicographical ordering of paths of unknown depth. }; - private String getCustomMessage(JsonNode schemaNode, String pname) { + private Map getCustomMessage(JsonNode schemaNode, String pname) { if (!this.validationContext.getConfig().isCustomMessageSupported()) { return null; } final JsonSchema parentSchema = getParentSchema(); final JsonNode message = getMessageNode(schemaNode, parentSchema, pname); - if (message != null && message.get(pname) != null) { - return message.get(pname).asText(); + if (message != null) { + JsonNode messageNode = message.get(pname); + if (messageNode != null) { + if (messageNode.isTextual()) { + return Collections.singletonMap("", messageNode.asText()); + } else if (messageNode.isObject()) { + Map result = new LinkedHashMap<>(); + messageNode.fields().forEachRemaining(entry -> { + result.put(entry.getKey(), entry.getValue().textValue()); + }); + if (!result.isEmpty()) { + return result; + } + } + } } - return null; + return Collections.emptyMap(); } private JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, String pname) { diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java index 0f9283b19..749d01387 100644 --- a/src/main/java/com/networknt/schema/Keyword.java +++ b/src/main/java/com/networknt/schema/Keyword.java @@ -17,12 +17,14 @@ package com.networknt.schema; +import java.util.Map; + import com.fasterxml.jackson.databind.JsonNode; public interface Keyword { String getValue(); - default void setCustomMessage(String message) { + default void setCustomMessage(Map message) { //setCustom message } diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index 46ef19a47..9990771b3 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -44,11 +44,11 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 > max) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 3ed565e1b..5a9330178 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -48,7 +48,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index e41d7b0bb..ce6b6acc6 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -44,7 +44,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + max)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index a4d900bb0..b3328c21e 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -116,8 +116,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMaximum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 77263ae63..d18c0a655 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -43,12 +43,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 6ce311085..5965c89e9 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index aecc86d4e..791b2f9a4 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -62,8 +62,8 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { return this.analysis != null ? this.analysis.stream() - .map(analysis -> buildValidationMessage(analysis.getAt(), analysis.getMessageKey(), - executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) + .map(analysis -> buildValidationMessage(null, analysis.getAt(), + analysis.getMessageKey(), executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) .collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index 61a6a491b..d23b573e6 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -44,7 +44,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + min)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 33001c8e5..2e0016d0a 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -123,8 +123,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - typedMinimum.thresholdValue())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 766a2c51a..383edd823 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -49,8 +49,8 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue)); BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), "" + divisor)); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), "" + divisor)); } } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index 29e1e84b5..a53c4db94 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode != null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); + errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 7d38e9f1d..89f18990e 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -46,8 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, at); errors = this.schema.validate(executionContext, node, rootNode, at); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), this.schema.toString())); + return Collections.singleton(buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), this.schema.toString())); } return Collections.emptySet(); } finally { @@ -66,8 +66,8 @@ public Set walk(ExecutionContext executionContext, JsonNode n Set errors = this.schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - this.schema.toString())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), this.schema.toString())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 4aac96170..068a2e51c 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -95,8 +95,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. if (numberOfValidSchema != 1) { - ValidationMessage message = buildValidationMessage(at, - executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); + ValidationMessage message = buildValidationMessage(null, + at, executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); if (this.failFast) { throw new JsonSchemaException(message); } diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index f57a1aa5a..362070391 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -61,7 +61,7 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { return Collections.singleton( - buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), this.pattern)); + buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), this.pattern)); } } catch (JsonSchemaException e) { throw e; diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index f1fb83f5d..765cb17eb 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -48,8 +48,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (msg.startsWith(path)) msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - errors.add(buildValidationMessage(schemaError.getPath(), - executionContext.getExecutionConfig().getLocale(), msg)); + errors.add(buildValidationMessage(pname, + schemaError.getPath(), executionContext.getExecutionConfig().getLocale(), msg)); } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 2b56e302e..31518fc59 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -41,7 +41,7 @@ public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); if (this.readOnly) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 3042acbe0..8a340363d 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -52,7 +52,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNode propertyNode = node.get(fieldName); if (propertyNode == null) { - errors.add(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), fieldName)); + errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); } } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 707be8421..97bcf5d77 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -60,8 +60,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - nodeType.toString(), this.schemaType.toString())); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), nodeType.toString(), this.schemaType.toString())); } // TODO: Is this really necessary? diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 1b00d3fe5..78783e318 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -98,7 +98,7 @@ private Set allPaths(JsonNode node, String at) { private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index ddcf0c1af..79e7517b1 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -97,7 +97,7 @@ private Set allPaths(JsonNode node, String at) { private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { List paths = new ArrayList<>(unevaluatedPaths); paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); } private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 7ee38be0f..48997c751 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -78,8 +78,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!valid) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), - nodeType.toString(), error)); + return Collections.singleton(buildValidationMessage(null, at, + executionContext.getExecutionConfig().getLocale(), nodeType.toString(), error)); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 5ad3efec5..5d5d3323a 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -45,7 +45,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } } } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 671a03d97..83c3cc9ce 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -59,7 +59,7 @@ public JsonSchema newSchema(String schemaPath, JsonNode schemaNode, JsonSchema p } public JsonValidator newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, String customMessage) { + JsonSchema parentSchema, Map customMessage) { return this.metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema, customMessage); } diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 004b165e9..bc0a6a520 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -131,7 +131,7 @@ public static ValidationMessage ofWithCustom(String type, ErrorMessageType error @Deprecated // Use the builder public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Object... arguments) { - return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage(), at, schemaPath, arguments); + return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage().get(""), at, schemaPath, arguments); } @Deprecated // Use the builder diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index f08ea25b8..4341979ed 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -5,10 +5,11 @@ import com.networknt.schema.utils.StringUtils; import java.util.Locale; +import java.util.Map; public abstract class ValidationMessageHandler { protected final boolean failFast; - protected final String customMessage; + protected final Map customMessage; protected final MessageSource messageSource; protected ValidatorTypeCode validatorType; protected ErrorMessageType errorMessageType; @@ -17,7 +18,7 @@ public abstract class ValidationMessageHandler { protected JsonSchema parentSchema; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, String customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { + protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, Map customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { this.failFast = failFast; this.errorMessageType = errorMessageType; this.customMessage = customMessage; @@ -27,11 +28,21 @@ protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessa this.parentSchema = parentSchema; } - protected ValidationMessage buildValidationMessage(String at, Locale locale, Object... arguments) { - return buildValidationMessage(at, getErrorMessageType().getErrorCodeValue(), locale, arguments); + protected ValidationMessage buildValidationMessage(String propertyName, String at, Locale locale, Object... arguments) { + return buildValidationMessage(propertyName, at, getErrorMessageType().getErrorCodeValue(), locale, arguments); } - protected ValidationMessage buildValidationMessage(String at, String messageKey, Locale locale, Object... arguments) { + protected ValidationMessage buildValidationMessage(String propertyName, String at, String messageKey, Locale locale, Object... arguments) { + String messagePattern = null; + if (this.customMessage != null) { + messagePattern = this.customMessage.get(""); + if (propertyName != null) { + String specificMessagePattern = this.customMessage.get(propertyName); + if (specificMessagePattern != null) { + messagePattern = specificMessagePattern; + } + } + } final ValidationMessage message = ValidationMessage.builder() .code(getErrorMessageType().getErrorCode()) .path(at) @@ -40,7 +51,7 @@ protected ValidationMessage buildValidationMessage(String at, String messageKey, .messageKey(messageKey) .messageFormatter(args -> this.messageSource.getMessage(messageKey, locale, args)) .type(getValidatorType().getValue()) - .message(this.customMessage) + .message(messagePattern) .build(); if (this.failFast && isApplicator()) { throw new JsonSchemaException(message); diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index f3a961617..f87006f67 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -117,7 +117,7 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { private final String value; private final String errorCode; - private String customMessage; + private Map customMessage; private final String errorCodeKey; private final Class validator; private final VersionCode versionCode; @@ -178,12 +178,12 @@ public String getErrorCode() { } @Override - public void setCustomMessage(String message) { + public void setCustomMessage(Map message) { this.customMessage = message; } @Override - public String getCustomMessage() { + public Map getCustomMessage() { return this.customMessage; } diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index aced43aba..3047a6ea3 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -25,7 +25,7 @@ public WriteOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema par public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { debug(logger, node, rootNode, at); if (this.writeOnly) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 670cf3e6e..0117d58e1 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -54,8 +54,8 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (!isLegalDateTime(node.textValue())) { - return Collections.singleton(buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), node.textValue(), - DATETIME)); + return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), + node.textValue(), DATETIME)); } return Collections.emptySet(); } diff --git a/src/test/resources/schema/customMessageTests/custom-message-tests.json b/src/test/resources/schema/customMessageTests/custom-message-tests.json index ffe99402f..1c3e90629 100644 --- a/src/test/resources/schema/customMessageTests/custom-message-tests.json +++ b/src/test/resources/schema/customMessageTests/custom-message-tests.json @@ -99,5 +99,98 @@ ] } ] + }, + { + "description": "messages for keywords for different properties", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "message": { + "type" : "should be an object", + "required": { + "foo" : "{0}: ''foo'' is required", + "bar" : "{0}: ''bar'' is required" + } + } + }, + "tests": [ + { + "description": "bar is required", + "data": { + "foo": 1 + }, + "valid": false, + "validationMessages": [ + "$: 'bar' is required" + ] + }, + { + "description": "foo is required", + "data": { + "bar": "bar" + }, + "valid": false, + "validationMessages": [ + "$: 'foo' is required" + ] + }, + { + "description": "both foo and bar are required", + "data": { + }, + "valid": false, + "validationMessages": [ + "$: 'foo' is required", + "$: 'bar' is required" + ] + } + ] + }, + { + "description": "messages for keywords with arguments", + "schema": { + "type": "object", + "properties": { + "requestedItems": { + "type": "array", + "minItems": 1, + "items": { + "properties": { + "item": { + "type": "string", + "minLength": 1, + "message": { + "minLength": "{0}: Item should not be empty" + } + } + } + } + } + } + }, + "tests": [ + { + "description": "should not be empty", + "data": { + "requestedItems": [ + { + "item": "" + } + ] + }, + "valid": false, + "validationMessages": [ + "$.requestedItems[0].item: Item should not be empty" + ] + } + ] } ] \ No newline at end of file