Skip to content

Commit 48ca8c3

Browse files
authored
Refactor validation message generation (#910)
* Add strategy for resolving locale specific messages * Support priority list * Use per execution locale * Support custom message per property
1 parent 0aaa967 commit 48ca8c3

File tree

71 files changed

+1243
-309
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+1243
-309
lines changed

doc/cust-msg.md

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ The json schema itself has a place for the customised message.
66
## Examples
77
### Example 1 :
88
The custom message can be provided outside properties for each type, as shown in the schema below.
9-
````json
9+
```json
1010
{
1111
"type": "object",
1212
"properties": {
@@ -24,10 +24,10 @@ The custom message can be provided outside properties for each type, as shown in
2424
"type" : "Invalid type"
2525
}
2626
}
27-
````
27+
```
2828
### Example 2 :
2929
To keep custom messages distinct for each type, one can even give them in each property.
30-
````json
30+
```json
3131
{
3232
"type": "object",
3333
"properties": {
@@ -47,14 +47,62 @@ To keep custom messages distinct for each type, one can even give them in each p
4747
}
4848
}
4949
}
50-
````
50+
```
51+
### Example 3 :
52+
For the keywords `required` and `dependencies`, different messages can be specified for different properties.
53+
54+
```json
55+
{
56+
"type": "object",
57+
"properties": {
58+
"foo": {
59+
"type": "number"
60+
},
61+
"bar": {
62+
"type": "string"
63+
}
64+
},
65+
"required": ["foo", "bar"],
66+
"message": {
67+
"type" : "should be an object",
68+
"required": {
69+
"foo" : "'foo' is required",
70+
"bar" : "'bar' is required"
71+
}
72+
}
73+
}
74+
```
75+
### Example 4 :
76+
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.
77+
78+
```json
79+
{
80+
"type": "object",
81+
"properties": {
82+
"foo": {
83+
"type": "number"
84+
},
85+
"bar": {
86+
"type": "string"
87+
}
88+
},
89+
"required": ["foo", "bar"],
90+
"message": {
91+
"type" : "should be an object",
92+
"required": {
93+
"foo" : "{0}: ''foo'' is required",
94+
"bar" : "{0}: ''bar'' is required"
95+
}
96+
}
97+
}
98+
```
5199

52100
## Format
53-
````json
101+
```json
54102
"message": {
55103
[validationType] : [customMessage]
56104
}
57-
````
105+
```
58106
Users can express custom message in the **'message'** field.
59107
The **'validation type'** should be the key and the **'custom message'** should be the value.
60108

doc/multiple-language.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,47 @@ JsonSchema schema = factory.getSchema(source, config);
2424
```
2525

2626
Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you
27-
choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that
28-
your resource bundle covers all the keys defined by the default bundle.
27+
choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that your resource bundle covers all the keys defined by the default bundle.
2928

3029
```
31-
// Set the configuration with a custom resource bundle (you can create this before each validation)
32-
ResourceBundle myBundle = ResourceBundle.getBundle("my-messages", myLocale);
30+
// Set the configuration with a custom message source
31+
MessageSource messageSource = new ResourceBundleMessageSource("my-messages");
3332
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
34-
config.setResourceBundle(myBundle);
33+
config.setMessageSource(messageSource);
3534
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
3635
JsonSchema schema = factory.getSchema(source, config);
3736
...
38-
```
37+
```
38+
39+
It is possible to override specific keys from the default resource bundle. Note however that you will need to supply all the languages for that specific key as it will not fallback on the default resource bundle. For instance the jsv-messages-override resource bundle will take precedence when resolving the message key.
40+
41+
```
42+
// Set the configuration with a custom message source
43+
MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME);
44+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
45+
config.setMessageSource(messageSource);
46+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
47+
JsonSchema schema = factory.getSchema(source, config);
48+
...
49+
```
50+
51+
The following approach can be used to determine the locale to use on a per user basis using a language tag priority list.
52+
53+
```
54+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
55+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
56+
JsonSchema schema = factory.getSchema(source, config);
57+
58+
// Uses the fr locale for this user
59+
Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0");
60+
ExecutionContext executionContext = jsonSchema.createExecutionContext();
61+
executionContext.getExecutionConfig().setLocale(locale);
62+
Set<ValidationMessage> messages = jsonSchema.validate(executionContext, rootNode);
63+
64+
// Uses the it locale for this user
65+
locale = Locales.findSupported("it;q=1.0,fr;q=0.9");
66+
executionContext = jsonSchema.createExecutionContext();
67+
executionContext.getExecutionConfig().setLocale(locale);
68+
messages = jsonSchema.validate(executionContext, rootNode);
69+
...
70+
```

src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
9797

9898
if (!allowedProperties.contains(pname) && !handledByPatternProperties) {
9999
if (!allowAdditionalProperties) {
100-
errors.add(buildValidationMessage(at, pname));
100+
errors.add(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), pname));
101101
} else {
102102
if (additionalPropertiesSchema != null) {
103103
ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY);

src/main/java/com/networknt/schema/AnyOfValidator.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
7777
//If schema has type validator and node type doesn't match with schemaType then ignore it
7878
//For union type, it is a must to call TypeValidator
7979
if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
80-
allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
80+
allErrors.add(buildValidationMessage(null, at,
81+
executionContext.getExecutionConfig().getLocale(), typeValidator.getSchemaType().toString()));
8182
continue;
8283
}
8384
}
@@ -106,7 +107,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
106107
if (this.discriminatorContext.isDiscriminatorMatchFound()) {
107108
if (!errors.isEmpty()) {
108109
allErrors.addAll(errors);
109-
allErrors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK));
110+
allErrors.add(buildValidationMessage(null,
111+
at, executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK));
110112
} else {
111113
// Clear all errors.
112114
allErrors.clear();
@@ -133,7 +135,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
133135

134136
if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) {
135137
final Set<ValidationMessage> errors = new HashSet<>();
136-
errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
138+
errors.add(buildValidationMessage(null, at,
139+
executionContext.getExecutionConfig().getLocale(), "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
137140
return Collections.unmodifiableSet(errors);
138141
}
139142
} finally {

src/main/java/com/networknt/schema/BaseJsonValidator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import com.fasterxml.jackson.databind.JsonNode;
2020
import com.fasterxml.jackson.databind.node.ObjectNode;
2121
import com.networknt.schema.ValidationContext.DiscriminatorContext;
22+
import com.networknt.schema.i18n.DefaultMessageSource;
23+
2224
import org.slf4j.Logger;
2325

2426
import java.net.URI;
@@ -47,7 +49,7 @@ public BaseJsonValidator(String schemaPath,
4749
ValidatorTypeCode validatorType,
4850
ValidationContext validationContext,
4951
boolean suppressSubSchemaRetrieval) {
50-
super(validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), validatorType, validatorType != null ? validatorType.getCustomMessage() : null, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getResourceBundle() : I18nSupport.DEFAULT_RESOURCE_BUNDLE, validatorType, parentSchema, schemaPath);
52+
super(validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), validatorType, validatorType != null ? validatorType.getCustomMessage() : null, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getMessageSource() : DefaultMessageSource.getInstance(), validatorType, parentSchema, schemaPath);
5153
this.schemaNode = schemaNode;
5254
this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval;
5355
this.applyDefaultsStrategy = (validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().getApplyDefaultsStrategy() != null) ? validationContext.getConfig().getApplyDefaultsStrategy() : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;

src/main/java/com/networknt/schema/ConstValidator.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import org.slf4j.LoggerFactory;
2121

2222
import java.util.Collections;
23-
import java.util.LinkedHashSet;
2423
import java.util.Set;
2524

2625
public class ConstValidator extends BaseJsonValidator implements JsonValidator {
@@ -35,14 +34,15 @@ public ConstValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS
3534
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) {
3635
debug(logger, node, rootNode, at);
3736

38-
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
3937
if (schemaNode.isNumber() && node.isNumber()) {
4038
if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) {
41-
errors.add(buildValidationMessage(at, schemaNode.asText()));
39+
return Collections.singleton(buildValidationMessage(null,
40+
at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText()));
4241
}
4342
} else if (!schemaNode.equals(node)) {
44-
errors.add(buildValidationMessage(at, schemaNode.asText()));
43+
return Collections.singleton(
44+
buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText()));
4545
}
46-
return Collections.unmodifiableSet(errors);
46+
return Collections.emptySet();
4747
}
4848
}

src/main/java/com/networknt/schema/ContainsValidator.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import java.util.Collection;
2525
import java.util.Collections;
26+
import java.util.Locale;
2627
import java.util.Optional;
2728
import java.util.Set;
2829

@@ -89,14 +90,16 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
8990
if(isMinV201909) {
9091
updateValidatorType(ValidatorTypeCode.MIN_CONTAINS);
9192
}
92-
return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), at, this.min);
93+
return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(),
94+
executionContext.getExecutionConfig().getLocale(), at, this.min);
9395
}
9496

9597
if (actual > this.max) {
9698
if(isMinV201909) {
9799
updateValidatorType(ValidatorTypeCode.MAX_CONTAINS);
98100
}
99-
return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), at, this.max);
101+
return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(),
102+
executionContext.getExecutionConfig().getLocale(), at, this.max);
100103
}
101104
}
102105

@@ -108,7 +111,7 @@ public void preloadJsonSchema() {
108111
Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators);
109112
}
110113

111-
private Set<ValidationMessage> boundsViolated(String messageKey, String at, int bounds) {
112-
return Collections.singleton(constructValidationMessage(messageKey, at, String.valueOf(bounds), this.schema.getSchemaNode().toString()));
114+
private Set<ValidationMessage> boundsViolated(String messageKey, Locale locale, String at, int bounds) {
115+
return Collections.singleton(buildValidationMessage(null, at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString()));
113116
}
114117
}

src/main/java/com/networknt/schema/DependenciesValidator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
6262
if (deps != null && !deps.isEmpty()) {
6363
for (String field : deps) {
6464
if (node.get(field) == null) {
65-
errors.add(buildValidationMessage(at, propertyDeps.toString()));
65+
errors.add(buildValidationMessage(pname, at,
66+
executionContext.getExecutionConfig().getLocale(), propertyDeps.toString()));
6667
}
6768
}
6869
}

src/main/java/com/networknt/schema/DependentRequired.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
5656
if (dependencies != null && !dependencies.isEmpty()) {
5757
for (String field : dependencies) {
5858
if (node.get(field) == null) {
59-
errors.add(buildValidationMessage(at, field, pname));
59+
errors.add(buildValidationMessage(pname, at, executionContext.getExecutionConfig().getLocale(),
60+
field, pname));
6061
}
6162
}
6263
}

src/main/java/com/networknt/schema/EnumValidator.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import java.util.Collections;
2626
import java.util.HashSet;
27-
import java.util.LinkedHashSet;
2827
import java.util.Set;
2928

3029
public class EnumValidator extends BaseJsonValidator implements JsonValidator {
@@ -81,13 +80,12 @@ public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSc
8180
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) {
8281
debug(logger, node, rootNode, at);
8382

84-
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
8583
if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue());
8684
if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) {
87-
errors.add(buildValidationMessage(at, error));
85+
return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), error));
8886
}
8987

90-
return Collections.unmodifiableSet(errors);
88+
return Collections.emptySet();
9189
}
9290

9391
/**

0 commit comments

Comments
 (0)