Skip to content

Refactor validation message generation #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
44 changes: 38 additions & 6 deletions doc/multiple-language.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,47 @@ JsonSchema schema = factory.getSchema(source, config);
```

Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you
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.
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.

```
// Set the configuration with a custom resource bundle (you can create this before each validation)
ResourceBundle myBundle = ResourceBundle.getBundle("my-messages", myLocale);
// Set the configuration with a custom message source
MessageSource messageSource = new ResourceBundleMessageSource("my-messages");
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setResourceBundle(myBundle);
config.setMessageSource(messageSource);
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
JsonSchema schema = factory.getSchema(source, config);
...
```
```

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.

```
// Set the configuration with a custom message source
MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME);
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setMessageSource(messageSource);
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
JsonSchema schema = factory.getSchema(source, config);
...
```

The following approach can be used to determine the locale to use on a per user basis using a language tag priority list.

```
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
JsonSchema schema = factory.getSchema(source, config);

// Uses the fr locale for this user
Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0");
ExecutionContext executionContext = jsonSchema.createExecutionContext();
executionContext.getExecutionConfig().setLocale(locale);
Set<ValidationMessage> messages = jsonSchema.validate(executionContext, rootNode);

// Uses the it locale for this user
locale = Locales.findSupported("it;q=1.0,fr;q=0.9");
executionContext = jsonSchema.createExecutionContext();
executionContext.getExecutionConfig().setLocale(locale);
messages = jsonSchema.validate(executionContext, rootNode);
...
```
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, 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
9 changes: 6 additions & 3 deletions src/main/java/com/networknt/schema/AnyOfValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +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, typeValidator.getSchemaType().toString()));
allErrors.add(buildValidationMessage(null, at,
executionContext.getExecutionConfig().getLocale(), typeValidator.getSchemaType().toString()));
continue;
}
}
Expand Down Expand Up @@ -106,7 +107,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
if (this.discriminatorContext.isDiscriminatorMatchFound()) {
if (!errors.isEmpty()) {
allErrors.addAll(errors);
allErrors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK));
allErrors.add(buildValidationMessage(null,
at, executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK));
} else {
// Clear all errors.
allErrors.clear();
Expand All @@ -133,7 +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, "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
4 changes: 3 additions & 1 deletion src/main/java/com/networknt/schema/BaseJsonValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.ValidationContext.DiscriminatorContext;
import com.networknt.schema.i18n.DefaultMessageSource;

import org.slf4j.Logger;

import java.net.URI;
Expand Down Expand Up @@ -47,7 +49,7 @@ public BaseJsonValidator(String schemaPath,
ValidatorTypeCode validatorType,
ValidationContext validationContext,
boolean suppressSubSchemaRetrieval) {
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);
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);
this.schemaNode = schemaNode;
this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval;
this.applyDefaultsStrategy = (validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().getApplyDefaultsStrategy() != null) ? validationContext.getConfig().getApplyDefaultsStrategy() : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY;
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/com/networknt/schema/ConstValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

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

Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
if (schemaNode.isNumber() && node.isNumber()) {
if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) {
errors.add(buildValidationMessage(at, schemaNode.asText()));
return Collections.singleton(buildValidationMessage(null,
at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText()));
}
} else if (!schemaNode.equals(node)) {
errors.add(buildValidationMessage(at, schemaNode.asText()));
return Collections.singleton(
buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText()));
}
return Collections.unmodifiableSet(errors);
return Collections.emptySet();
}
}
11 changes: 7 additions & 4 deletions src/main/java/com/networknt/schema/ContainsValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import java.util.Collection;
import java.util.Collections;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;

Expand Down Expand Up @@ -89,14 +90,16 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
if(isMinV201909) {
updateValidatorType(ValidatorTypeCode.MIN_CONTAINS);
}
return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), at, this.min);
return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(),
executionContext.getExecutionConfig().getLocale(), at, this.min);
}

if (actual > this.max) {
if(isMinV201909) {
updateValidatorType(ValidatorTypeCode.MAX_CONTAINS);
}
return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), at, this.max);
return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(),
executionContext.getExecutionConfig().getLocale(), at, this.max);
}
}

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

private Set<ValidationMessage> boundsViolated(String messageKey, String at, int bounds) {
return Collections.singleton(constructValidationMessage(messageKey, at, String.valueOf(bounds), this.schema.getSchemaNode().toString()));
private Set<ValidationMessage> boundsViolated(String messageKey, Locale locale, String at, int bounds) {
return Collections.singleton(buildValidationMessage(null, at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +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, propertyDeps.toString()));
errors.add(buildValidationMessage(pname, at,
executionContext.getExecutionConfig().getLocale(), propertyDeps.toString()));
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/networknt/schema/DependentRequired.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +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, field, pname));
errors.add(buildValidationMessage(pname, at, executionContext.getExecutionConfig().getLocale(),
field, pname));
}
}
}
Expand Down
6 changes: 2 additions & 4 deletions src/main/java/com/networknt/schema/EnumValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

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

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

return Collections.unmodifiableSet(errors);
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,7 +107,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
}

if (typedMaximum.crossesThreshold(node)) {
return Collections.singleton(buildValidationMessage(at, 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,7 +114,8 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
}

if (typedMinimum.crossesThreshold(node)) {
return Collections.singleton(buildValidationMessage(at, typedMinimum.thresholdValue()));
return Collections.singleton(buildValidationMessage(null, at,
executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue()));
}
return Collections.emptySet();
}
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/networknt/schema/ExecutionConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.networknt.schema;

import java.util.Locale;
import java.util.Objects;

/**
* Configuration per execution.
*/
public class ExecutionConfig {
private Locale locale = Locale.ROOT;

public Locale getLocale() {
return locale;
}

public void setLocale(Locale locale) {
this.locale = Objects.requireNonNull(locale, "Locale must not be null");
}

}
Loading