Skip to content

Commit

Permalink
Support custom message per property
Browse files Browse the repository at this point in the history
  • Loading branch information
justin-tay committed Dec 14, 2023
1 parent 08cf64d commit a184ea1
Show file tree
Hide file tree
Showing 47 changed files with 264 additions and 94 deletions.
60 changes: 54 additions & 6 deletions doc/cust-msg.md
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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": {
Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public Set<ValidationMessage> 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);
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/networknt/schema/AnyOfValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ public Set<ValidationMessage> 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;
}
}
Expand Down Expand Up @@ -107,8 +107,8 @@ public Set<ValidationMessage> 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();
Expand All @@ -135,8 +135,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo

if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) {
final Set<ValidationMessage> 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 {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/com/networknt/schema/ConstValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ public Set<ValidationMessage> 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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/ContainsValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,6 @@ public void preloadJsonSchema() {
}

private Set<ValidationMessage> 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()));
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/networknt/schema/DependenciesValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ public Set<ValidationMessage> 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()));
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/networknt/schema/DependentRequired.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ public Set<ValidationMessage> 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));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/EnumValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public Set<ValidationMessage> 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();
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/networknt/schema/ErrorMessageType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,7 +28,7 @@ public interface ErrorMessageType {
*/
String getErrorCode();

default String getCustomMessage() {
default Map<String, String> getCustomMessage() {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ public Set<ValidationMessage> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ public Set<ValidationMessage> 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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/FalseValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ public FalseValidator(String schemaPath, final JsonNode schemaNode, JsonSchema p
public Set<ValidationMessage> 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()));
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/FormatKeyword.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public String getValue() {
}

@Override
public void setCustomMessage(String message) {
public void setCustomMessage(Map<String, String> message) {
this.type.setCustomMessage(message);
}
}
12 changes: 6 additions & 6 deletions src/main/java/com/networknt/schema/FormatValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,18 @@ public Set<ValidationMessage> 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
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/ItemsValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ private void doValidate(ExecutionContext executionContext, Set<ValidationMessage
} else {
// no additional item allowed, return error
errors.add(
buildValidationMessage(at, executionContext.getExecutionConfig().getLocale(), "" + i));
buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + i));
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/JsonMetaSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ public Map<String, Keyword> getKeywords() {
}

public JsonValidator newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode,
JsonSchema parentSchema, String customMessage) {
JsonSchema parentSchema, Map<String, String> customMessage) {

try {
Keyword kw = this.keywords.get(keyword);
Expand Down
28 changes: 21 additions & 7 deletions src/main/java/com/networknt/schema/JsonSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -258,11 +259,11 @@ private Map<String, JsonValidator> read(JsonNode schemaNode) {
Map<String, JsonValidator> validators = new TreeMap<>(VALIDATOR_SORT);
if (schemaNode.isBoolean()) {
if (schemaNode.booleanValue()) {
final String customMessage = getCustomMessage(schemaNode, "true");
final Map<String, String> 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<String, String> customMessage = getCustomMessage(schemaNode, "false");
JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage);
validators.put(getSchemaPath() + "/false", validator);
}
Expand All @@ -276,7 +277,7 @@ private Map<String, JsonValidator> 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<String, String> customMessage = getCustomMessage(schemaNode, pname);

if ("$recursiveAnchor".equals(pname)) {
if (!nodeToUse.isBoolean()) {
Expand Down Expand Up @@ -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<String, String> 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<String, String> 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) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/com/networknt/schema/Keyword.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> message) {
//setCustom message
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/networknt/schema/MaxItemsValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public Set<ValidationMessage> 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));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/networknt/schema/MaxLengthValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public Set<ValidationMessage> 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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public Set<ValidationMessage> 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));
}
}

Expand Down
Loading

0 comments on commit a184ea1

Please sign in to comment.