diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index c456f18fa..cd4d9c1f2 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -89,7 +89,7 @@ protected Set validate(ExecutionContext executionContext, Jso final ObjectNode discriminator = currentDiscriminatorContext .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); if (null != discriminator) { - registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, + DiscriminatorValidator.registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, this.parentSchema, instanceLocation); // now we have to check whether we have hit the right target final String discriminatorPropertyName = discriminator.get("propertyName").asText(); @@ -98,7 +98,7 @@ protected Set validate(ExecutionContext executionContext, Jso : discriminatorNode.textValue(); final JsonSchema jsonSchema = this.parentSchema; - checkDiscriminatorMatch(currentDiscriminatorContext, discriminator, + DiscriminatorValidator.checkDiscriminatorMatch(currentDiscriminatorContext, discriminator, discriminatorPropertyValue, jsonSchema); } } diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index ca1c33867..c1313ff44 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -17,61 +17,65 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.annotation.JsonNodeAnnotation; -import com.networknt.schema.i18n.DefaultMessageSource; -import com.networknt.schema.i18n.MessageSource; import org.slf4j.Logger; import java.util.Collection; -import java.util.Iterator; import java.util.Map; -import java.util.Set; import java.util.function.Consumer; /** * Base {@link JsonValidator}. */ -public abstract class BaseJsonValidator extends ValidationMessageHandler implements JsonValidator { - protected final boolean suppressSubSchemaRetrieval; - +public abstract class BaseJsonValidator implements JsonValidator { protected final JsonNode schemaNode; protected final ValidationContext validationContext; + protected final Keyword keyword; + protected final JsonSchema parentSchema; + protected final SchemaLocation schemaLocation; + protected final Map errorMessage; + + protected final JsonNodePath evaluationPath; + protected final JsonSchema evaluationParentSchema; + + // Pending removal + protected final ErrorMessageType errorMessageType; + public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) { - this(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType, validatorType, validationContext, - false); + this(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType, validatorType, validationContext); } public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, - ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(errorMessageType, - (validationContext != null && validationContext.getConfig() != null) - ? validationContext.getConfig().getErrorMessageKeyword() - : null, - (validationContext != null && validationContext.getConfig() != null) - ? validationContext.getConfig().getMessageSource() - : DefaultMessageSource.getInstance(), - keyword, - parentSchema, schemaLocation, evaluationPath); + ValidationContext validationContext) { this.validationContext = validationContext; this.schemaNode = schemaNode; - this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; + + this.keyword = keyword; + this.parentSchema = parentSchema; + this.schemaLocation = schemaLocation; + if (keyword != null && parentSchema != null && validationContext.getConfig().getErrorMessageKeyword() != null) { + this.errorMessage = ErrorMessages.getErrorMessage(parentSchema, + validationContext.getConfig().getErrorMessageKeyword(), keyword.getValue()); + } else { + this.errorMessage = null; + } + + this.errorMessageType = errorMessageType; + this.evaluationPath = evaluationPath; + this.evaluationParentSchema = null; } /** * Constructor to create a copy using fields. * - * @param suppressSubSchemaRetrieval to suppress sub schema retrieval * @param schemaNode the schema node * @param validationContext the validation context * @param errorMessageType the error message type - * @param errorMessageKeyword the error message keyword - * @param messageSource the message source * @param keyword the keyword * @param parentSchema the parent schema * @param schemaLocation the schema location @@ -81,45 +85,27 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP */ protected BaseJsonValidator( /* Below from BaseJsonValidator */ - boolean suppressSubSchemaRetrieval, JsonNode schemaNode, ValidationContext validationContext, /* Below from ValidationMessageHandler */ ErrorMessageType errorMessageType, - String errorMessageKeyword, - MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { - super(errorMessageType, errorMessageKeyword, messageSource, keyword, - parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); - this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; this.schemaNode = schemaNode; this.validationContext = validationContext; - } - - private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) { - final JsonNode node = schemaNode.get("id"); - - if (node == null) { - return null; - } - - if (node.equals(schemaNode.get("$schema"))) { - return null; - } - - final String text = node.textValue(); - if (text == null) { - return null; - } - - final SchemaLocation schemaLocation = SchemaLocation.of(node.textValue()); - - return validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); + + this.keyword = keyword; + this.parentSchema = parentSchema; + this.schemaLocation = schemaLocation; + this.errorMessage = errorMessage; + + this.errorMessageType = errorMessageType; + this.evaluationPath = evaluationPath; + this.evaluationParentSchema = evaluationParentSchema; } protected static boolean equals(double n1, double n2) { @@ -145,129 +131,6 @@ public static void debug(Logger logger, ExecutionContext executionContext, JsonN } } - /** - * Checks based on the current {@link DiscriminatorContext} whether the provided {@link JsonSchema} a match against - * the current discriminator. - * - * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} - * @param discriminator the discriminator to use for the check - * @param discriminatorPropertyValue the value of the discriminator/propertyName field - * @param jsonSchema the {@link JsonSchema} to check - */ - protected static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext, - final ObjectNode discriminator, - final String discriminatorPropertyValue, - final JsonSchema jsonSchema) { - if (discriminatorPropertyValue == null) { - currentDiscriminatorContext.markIgnore(); - return; - } - - final JsonNode discriminatorMapping = discriminator.get("mapping"); - if (null == discriminatorMapping) { - checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, - discriminatorPropertyValue, - jsonSchema); - } else { - checkForExplicitDiscriminatorMappingMatch(currentDiscriminatorContext, - discriminatorPropertyValue, - discriminatorMapping, - jsonSchema); - if (!currentDiscriminatorContext.isDiscriminatorMatchFound() - && noExplicitDiscriminatorKeyOverride(discriminatorMapping, jsonSchema)) { - checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, - discriminatorPropertyValue, - jsonSchema); - } - } - } - - /** - * Rolls up all nested and compatible discriminators to the root discriminator of the type. Detects attempts to redefine - * the propertyName or mappings. - * - * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} - * @param discriminator the discriminator to use for the check - * @param schema the value of the discriminator/propertyName field - * @param instanceLocation the logging prefix - */ - protected static void registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext, - final ObjectNode discriminator, - final JsonSchema schema, - final JsonNodePath instanceLocation) { - final JsonNode discriminatorOnSchema = schema.schemaNode.get("discriminator"); - if (null != discriminatorOnSchema && null != currentDiscriminatorContext - .getDiscriminatorForPath(schema.schemaLocation)) { - // this is where A -> B -> C inheritance exists, A has the root discriminator and B adds to the mapping - final JsonNode propertyName = discriminatorOnSchema.get("propertyName"); - if (null != propertyName) { - throw new JsonSchemaException(instanceLocation + " schema " + schema + " attempts redefining the discriminator property"); - } - final ObjectNode mappingOnContextDiscriminator = (ObjectNode) discriminator.get("mapping"); - final ObjectNode mappingOnCurrentSchemaDiscriminator = (ObjectNode) discriminatorOnSchema.get("mapping"); - if (null == mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { - // here we have a mapping on a nested discriminator and none on the root discriminator, so we can simply - // make it the root's - discriminator.set("mapping", discriminatorOnSchema); - } else if (null != mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { - // here we have to merge. The spec doesn't specify anything on this, but here we don't accept redefinition of - // mappings that already exist - final Iterator> fieldsToAdd = mappingOnCurrentSchemaDiscriminator.fields(); - while (fieldsToAdd.hasNext()) { - final Map.Entry fieldToAdd = fieldsToAdd.next(); - final String mappingKeyToAdd = fieldToAdd.getKey(); - final JsonNode mappingValueToAdd = fieldToAdd.getValue(); - - final JsonNode currentMappingValue = mappingOnContextDiscriminator.get(mappingKeyToAdd); - if (null != currentMappingValue && currentMappingValue != mappingValueToAdd) { - throw new JsonSchemaException(instanceLocation + "discriminator mapping redefinition from " + mappingKeyToAdd - + "/" + currentMappingValue + " to " + mappingValueToAdd); - } else if (null == currentMappingValue) { - mappingOnContextDiscriminator.set(mappingKeyToAdd, mappingValueToAdd); - } - } - } - } - currentDiscriminatorContext.registerDiscriminator(schema.schemaLocation, discriminator); - } - - private static void checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, - final String discriminatorPropertyValue, - final JsonSchema schema) { - if (schema.schemaLocation.getFragment().getName(-1).equals(discriminatorPropertyValue)) { - currentDiscriminatorContext.markMatch(); - } - } - - private static void checkForExplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, - final String discriminatorPropertyValue, - final JsonNode discriminatorMapping, - final JsonSchema schema) { - final Iterator> explicitMappings = discriminatorMapping.fields(); - while (explicitMappings.hasNext()) { - final Map.Entry candidateExplicitMapping = explicitMappings.next(); - if (candidateExplicitMapping.getKey().equals(discriminatorPropertyValue) - && ("#" + schema.schemaLocation.getFragment().toString()) - .equals(candidateExplicitMapping.getValue().asText())) { - currentDiscriminatorContext.markMatch(); - break; - } - } - } - - private static boolean noExplicitDiscriminatorKeyOverride(final JsonNode discriminatorMapping, - final JsonSchema parentSchema) { - final Iterator> explicitMappings = discriminatorMapping.fields(); - while (explicitMappings.hasNext()) { - final Map.Entry candidateExplicitMapping = explicitMappings.next(); - if (candidateExplicitMapping.getValue().asText() - .equals(parentSchema.schemaLocation.getFragment().toString())) { - return false; - } - } - return true; - } - @Override public SchemaLocation getSchemaLocation() { return this.schemaLocation; @@ -313,14 +176,6 @@ public JsonSchema getEvaluationParentSchema() { return getParentSchema(); } - protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) { - return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext); - } - - public Set validate(ExecutionContext executionContext, JsonNode node) { - return validate(executionContext, node, node, atRoot()); - } - protected String getNodeFieldType() { JsonNode typeField = this.getParentSchema().getSchemaNode().get("type"); if (typeField != null) { @@ -335,42 +190,7 @@ protected void preloadJsonSchemas(final Collection schemas) { } } - public static class JsonNodePathLegacy { - private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.LEGACY); - public static JsonNodePath getInstance() { - return INSTANCE; - } - } - public static class JsonNodePathJsonPointer { - private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_POINTER); - public static JsonNodePath getInstance() { - return INSTANCE; - } - } - - public static class JsonNodePathJsonPath { - private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_PATH); - public static JsonNodePath getInstance() { - return INSTANCE; - } - } - - /** - * Get the root path. - * - * @return The path. - */ - protected JsonNodePath atRoot() { - if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_POINTER)) { - return JsonNodePathJsonPointer.getInstance(); - } else if (this.validationContext.getConfig().getPathType().equals(PathType.LEGACY)) { - return JsonNodePathLegacy.getInstance(); - } else if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_PATH)) { - return JsonNodePathJsonPath.getInstance(); - } - return new JsonNodePath(this.validationContext.getConfig().getPathType()); - } @Override public String toString() { @@ -408,9 +228,14 @@ protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { return false; } - @Override protected MessageSourceValidationMessage.Builder message() { - return super.message().schemaNode(this.schemaNode); + return MessageSourceValidationMessage.builder(this.validationContext.getConfig().getMessageSource(), this.errorMessage, (message, failFast) -> { + if (failFast) { + throw new FailFastAssertionException(message); + } + }).code(this.errorMessageType.getErrorCode()).schemaNode(this.schemaNode).schemaLocation(this.schemaLocation) + .evaluationPath(this.evaluationPath).type(this.keyword != null ? this.keyword.getValue() : null) + .messageKey(this.errorMessageType.getErrorCodeValue()); } /** diff --git a/src/main/java/com/networknt/schema/DiscriminatorValidator.java b/src/main/java/com/networknt/schema/DiscriminatorValidator.java index d4b1135fe..8cc08df18 100644 --- a/src/main/java/com/networknt/schema/DiscriminatorValidator.java +++ b/src/main/java/com/networknt/schema/DiscriminatorValidator.java @@ -81,4 +81,127 @@ public String getPropertyName() { public Map getMapping() { return mapping; } + + /** + * Checks based on the current {@link DiscriminatorContext} whether the provided {@link JsonSchema} a match against + * the current discriminator. + * + * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} + * @param discriminator the discriminator to use for the check + * @param discriminatorPropertyValue the value of the discriminator/propertyName field + * @param jsonSchema the {@link JsonSchema} to check + */ + public static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext, + final ObjectNode discriminator, + final String discriminatorPropertyValue, + final JsonSchema jsonSchema) { + if (discriminatorPropertyValue == null) { + currentDiscriminatorContext.markIgnore(); + return; + } + + final JsonNode discriminatorMapping = discriminator.get("mapping"); + if (null == discriminatorMapping) { + checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, + discriminatorPropertyValue, + jsonSchema); + } else { + checkForExplicitDiscriminatorMappingMatch(currentDiscriminatorContext, + discriminatorPropertyValue, + discriminatorMapping, + jsonSchema); + if (!currentDiscriminatorContext.isDiscriminatorMatchFound() + && noExplicitDiscriminatorKeyOverride(discriminatorMapping, jsonSchema)) { + checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext, + discriminatorPropertyValue, + jsonSchema); + } + } + } + + /** + * Rolls up all nested and compatible discriminators to the root discriminator of the type. Detects attempts to redefine + * the propertyName or mappings. + * + * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext} + * @param discriminator the discriminator to use for the check + * @param schema the value of the discriminator/propertyName field + * @param instanceLocation the logging prefix + */ + public static void registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext, + final ObjectNode discriminator, + final JsonSchema schema, + final JsonNodePath instanceLocation) { + final JsonNode discriminatorOnSchema = schema.schemaNode.get("discriminator"); + if (null != discriminatorOnSchema && null != currentDiscriminatorContext + .getDiscriminatorForPath(schema.schemaLocation)) { + // this is where A -> B -> C inheritance exists, A has the root discriminator and B adds to the mapping + final JsonNode propertyName = discriminatorOnSchema.get("propertyName"); + if (null != propertyName) { + throw new JsonSchemaException(instanceLocation + " schema " + schema + " attempts redefining the discriminator property"); + } + final ObjectNode mappingOnContextDiscriminator = (ObjectNode) discriminator.get("mapping"); + final ObjectNode mappingOnCurrentSchemaDiscriminator = (ObjectNode) discriminatorOnSchema.get("mapping"); + if (null == mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { + // here we have a mapping on a nested discriminator and none on the root discriminator, so we can simply + // make it the root's + discriminator.set("mapping", discriminatorOnSchema); + } else if (null != mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) { + // here we have to merge. The spec doesn't specify anything on this, but here we don't accept redefinition of + // mappings that already exist + final Iterator> fieldsToAdd = mappingOnCurrentSchemaDiscriminator.fields(); + while (fieldsToAdd.hasNext()) { + final Map.Entry fieldToAdd = fieldsToAdd.next(); + final String mappingKeyToAdd = fieldToAdd.getKey(); + final JsonNode mappingValueToAdd = fieldToAdd.getValue(); + + final JsonNode currentMappingValue = mappingOnContextDiscriminator.get(mappingKeyToAdd); + if (null != currentMappingValue && currentMappingValue != mappingValueToAdd) { + throw new JsonSchemaException(instanceLocation + "discriminator mapping redefinition from " + mappingKeyToAdd + + "/" + currentMappingValue + " to " + mappingValueToAdd); + } else if (null == currentMappingValue) { + mappingOnContextDiscriminator.set(mappingKeyToAdd, mappingValueToAdd); + } + } + } + } + currentDiscriminatorContext.registerDiscriminator(schema.schemaLocation, discriminator); + } + + private static void checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, + final String discriminatorPropertyValue, + final JsonSchema schema) { + if (schema.schemaLocation.getFragment().getName(-1).equals(discriminatorPropertyValue)) { + currentDiscriminatorContext.markMatch(); + } + } + + private static void checkForExplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, + final String discriminatorPropertyValue, + final JsonNode discriminatorMapping, + final JsonSchema schema) { + final Iterator> explicitMappings = discriminatorMapping.fields(); + while (explicitMappings.hasNext()) { + final Map.Entry candidateExplicitMapping = explicitMappings.next(); + if (candidateExplicitMapping.getKey().equals(discriminatorPropertyValue) + && ("#" + schema.schemaLocation.getFragment().toString()) + .equals(candidateExplicitMapping.getValue().asText())) { + currentDiscriminatorContext.markMatch(); + break; + } + } + } + + private static boolean noExplicitDiscriminatorKeyOverride(final JsonNode discriminatorMapping, + final JsonSchema parentSchema) { + final Iterator> explicitMappings = discriminatorMapping.fields(); + while (explicitMappings.hasNext()) { + final Map.Entry candidateExplicitMapping = explicitMappings.next(); + if (candidateExplicitMapping.getValue().asText() + .equals(parentSchema.schemaLocation.getFragment().toString())) { + return false; + } + } + return true; + } } diff --git a/src/main/java/com/networknt/schema/ErrorMessages.java b/src/main/java/com/networknt/schema/ErrorMessages.java new file mode 100644 index 000000000..3cda00848 --- /dev/null +++ b/src/main/java/com/networknt/schema/ErrorMessages.java @@ -0,0 +1,58 @@ +package com.networknt.schema; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; + +/** + * ErrorMessages. + */ +public class ErrorMessages { + /** + * Gets the custom error message to use. + * + * @param parentSchema the parent schema + * @param errorMessageKeyword the error message keyword + * @param keyword the keyword + * @return the custom error message + */ + public static Map getErrorMessage(JsonSchema parentSchema, String errorMessageKeyword, + String keyword) { + final JsonNode message = getMessageNode(errorMessageKeyword, parentSchema.schemaNode, parentSchema, keyword); + if (message != null) { + JsonNode messageNode = message.get(keyword); + 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 Collections.emptyMap(); + } + + protected static JsonNode getMessageNode(String errorMessageKeyword, JsonNode schemaNode, JsonSchema parentSchema, + String pname) { + if (schemaNode.get(errorMessageKeyword) != null && schemaNode.get(errorMessageKeyword).get(pname) != null) { + return schemaNode.get(errorMessageKeyword); + } + JsonNode messageNode; + messageNode = schemaNode.get(errorMessageKeyword); + if (messageNode == null && parentSchema != null) { + messageNode = parentSchema.schemaNode.get(errorMessageKeyword); + if (messageNode == null) { + return getMessageNode(errorMessageKeyword, parentSchema.schemaNode, parentSchema.getParentSchema(), + pname); + } + } + return messageNode; + } +} diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 65d67b284..a3627e58f 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -16,13 +16,6 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.i18n.MessageSource; -import com.networknt.schema.utils.JsonNodes; -import com.networknt.schema.utils.SetView; - import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -38,6 +31,12 @@ import java.util.Set; import java.util.function.Consumer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.utils.JsonNodes; +import com.networknt.schema.utils.SetView; + /** * Used for creating a schema with validators for validating inputs. This is * created using {@link JsonSchemaFactory#getInstance(VersionFlag, Consumer)} @@ -50,7 +49,7 @@ * JsonSchema instances are thread-safe provided its configuration is not * modified. */ -public class JsonSchema extends BaseJsonValidator { +public class JsonSchema implements JsonSchemaValidator { private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue(); private final String id; @@ -62,6 +61,107 @@ public class JsonSchema extends BaseJsonValidator { private boolean recursiveAnchor = false; private TypeValidator typeValidator = null; + protected final JsonNode schemaNode; + protected final JsonSchema parentSchema; + protected final SchemaLocation schemaLocation; + protected final ValidationContext validationContext; + protected final boolean suppressSubSchemaRetrieval; + + protected final JsonNodePath evaluationPath; + protected final JsonSchema evaluationParentSchema; + + public JsonNode getSchemaNode() { + return this.schemaNode; + } + + public SchemaLocation getSchemaLocation() { + return this.schemaLocation; + } + + public JsonSchema getParentSchema() { + return parentSchema; + } + + public boolean isSuppressSubSchemaRetrieval() { + return suppressSubSchemaRetrieval; + } + + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + public JsonSchema getEvaluationParentSchema() { + if (this.evaluationParentSchema != null) { + return this.evaluationParentSchema; + } + return getParentSchema(); + } + + protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) { + return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext); + } + + private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) { + final JsonNode node = schemaNode.get("id"); + + if (node == null) { + return null; + } + + if (node.equals(schemaNode.get("$schema"))) { + return null; + } + + final String text = node.textValue(); + if (text == null) { + return null; + } + + final SchemaLocation schemaLocation = SchemaLocation.of(node.textValue()); + + return validationContext.getJsonSchemaFactory().getSchema(schemaLocation, validationContext.getConfig()); + } + public static class JsonNodePathLegacy { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.LEGACY); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + + public static class JsonNodePathJsonPointer { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_POINTER); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + + public static class JsonNodePathJsonPath { + private static final JsonNodePath INSTANCE = new JsonNodePath(PathType.JSON_PATH); + public static JsonNodePath getInstance() { + return INSTANCE; + } + } + + public Set validate(ExecutionContext executionContext, JsonNode node) { + return validate(executionContext, node, node, atRoot()); + } + + /** + * Get the root path. + * + * @return The path. + */ + protected JsonNodePath atRoot() { + if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_POINTER)) { + return JsonNodePathJsonPointer.getInstance(); + } else if (this.validationContext.getConfig().getPathType().equals(PathType.LEGACY)) { + return JsonNodePathLegacy.getInstance(); + } else if (this.validationContext.getConfig().getPathType().equals(PathType.JSON_PATH)) { + return JsonNodePathJsonPath.getInstance(); + } + return new JsonNodePath(this.validationContext.getConfig().getPathType()); + } + static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { return new JsonSchema(validationContext, schemaLocation, evaluationPath, schemaNode, parent, suppressSubSchemaRetrieval); } @@ -107,8 +207,18 @@ private static SchemaLocation resolve(SchemaLocation schemaLocation, JsonNode sc private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { + /* super(resolve(schemaLocation, schemaNode, parent == null, validationContext), evaluationPath, schemaNode, parent, null, null, validationContext, suppressSubSchemaRetrieval); + */ + this.validationContext = validationContext; + this.schemaLocation = resolve(schemaLocation, schemaNode, parent == null, validationContext); + this.schemaNode = schemaNode; + this.parentSchema = parent; + this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; + this.evaluationPath = evaluationPath; + this.evaluationParentSchema = null; + String id = this.validationContext.resolveSchemaId(this.schemaNode); if (id != null) { // In earlier drafts $id may contain an anchor fragment see draft4/idRef.json @@ -163,8 +273,6 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc * @param schemaNode the schema node * @param validationContext the validation context * @param errorMessageType the error message type - * @param errorMessageKeyword the error message keyword - * @param messageSource the message source * @param keyword the keyword * @param parentSchema the parent schema * @param schemaLocation the schema location @@ -185,21 +293,27 @@ protected JsonSchema( ValidationContext validationContext, /* Below from ValidationMessageHandler */ ErrorMessageType errorMessageType, - String errorMessageKeyword, - MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { - super(suppressSubSchemaRetrieval, schemaNode, validationContext, errorMessageType, errorMessageKeyword, messageSource, keyword, - parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); +// super(suppressSubSchemaRetrieval, schemaNode, validationContext, errorMessageType, keyword, +// parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, errorMessage); this.validators = validators; this.validatorsLoaded = validatorsLoaded; this.recursiveAnchor = recursiveAnchor; this.typeValidator = typeValidator; this.id = id; + + this.validationContext = validationContext; + this.schemaLocation = schemaLocation; + this.schemaNode = schemaNode; + this.parentSchema = parentSchema; + this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; + this.evaluationPath = evaluationPath; + this.evaluationParentSchema = evaluationParentSchema; } /** @@ -239,9 +353,9 @@ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath ref schemaNode, validationContext, /* Below from ValidationMessageHandler */ - errorMessageType, errorMessageKeyword, messageSource, - keyword, parentSchema, schemaLocation, evaluationPath, - evaluationParentSchema, errorMessage); + /*errorMessageType*/ null, + /*keyword*/ null, parentSchema, schemaLocation, evaluationPath, + evaluationParentSchema, /* errorMessage */ null); } public JsonSchema withConfig(SchemaValidatorsConfig config) { @@ -273,15 +387,13 @@ public JsonSchema withConfig(SchemaValidatorsConfig config) { schemaNode, validationContext, /* Below from ValidationMessageHandler */ - errorMessageType, - errorMessageKeyword, - messageSource, - keyword, + /* errorMessageType */ null, + /* keyword */ null, parentSchema, schemaLocation, evaluationPath, evaluationParentSchema, - errorMessage); + /* errorMessage */ null); } return this; @@ -647,7 +759,7 @@ public Set validate(ExecutionContext executionContext, JsonNo final JsonNode discriminatorNode = jsonNode.get(discriminatorPropertyName); final String discriminatorPropertyValue = discriminatorNode == null ? null : discriminatorNode.asText(); - checkDiscriminatorMatch(discriminatorContext, discriminatorToUse, discriminatorPropertyValue, + DiscriminatorValidator.checkDiscriminatorMatch(discriminatorContext, discriminatorToUse, discriminatorPropertyValue, this); } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaValidator.java b/src/main/java/com/networknt/schema/JsonSchemaValidator.java new file mode 100644 index 000000000..3216b1ac0 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonSchemaValidator.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2016 Network New Technologies Inc. + * + * 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.Collections; +import java.util.Set; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.walk.JsonSchemaWalker; + +/** + * Standard json validator interface, implemented by all validators and JsonSchema. + */ +public interface JsonSchemaValidator extends JsonSchemaWalker { + /** + * Validate the given JsonNode, the given node is the child node of the root node at given + * data path. + * @param executionContext ExecutionContext + * @param node JsonNode + * @param rootNode JsonNode + * @param instanceLocation JsonNodePath + * + * @return A list of ValidationMessage if there is any validation error, or an empty + * list if there is no error. + */ + Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation); + + /** + * This is default implementation of walk method. Its job is to call the + * validate method if shouldValidateSchema is enabled. + */ + @Override + default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + if (node == null) { + // Note that null is not the same as NullNode + return Collections.emptySet(); + } + return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) + : Collections.emptySet(); + } + + /** + * The schema location is the canonical URI of the schema object plus a JSON + * Pointer fragment indicating the subschema that produced a result. In contrast + * with the evaluation path, the schema location MUST NOT include by-reference + * applicators such as $ref or $dynamicRef. + * + * @return the schema location + */ + SchemaLocation getSchemaLocation(); + + /** + * The evaluation path is the set of keys, starting from the schema root, + * through which evaluation passes to reach the schema object that produced a + * specific result. + * + * @return the evaluation path + */ + JsonNodePath getEvaluationPath(); +} diff --git a/src/main/java/com/networknt/schema/JsonValidator.java b/src/main/java/com/networknt/schema/JsonValidator.java index 43b180bca..d81d01bb8 100644 --- a/src/main/java/com/networknt/schema/JsonValidator.java +++ b/src/main/java/com/networknt/schema/JsonValidator.java @@ -16,30 +16,10 @@ package com.networknt.schema; -import java.util.Collections; -import java.util.Set; - -import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.walk.JsonSchemaWalker; - /** - * Standard json validator interface, implemented by all validators and JsonSchema. + * KeywordValidator interface implemented by all keyword validators. */ -public interface JsonValidator extends JsonSchemaWalker { - /** - * Validate the given JsonNode, the given node is the child node of the root node at given - * data path. - * @param executionContext ExecutionContext - * @param node JsonNode - * @param rootNode JsonNode - * @param instanceLocation JsonNodePath - * - * @return A list of ValidationMessage if there is any validation error, or an empty - * list if there is no error. - */ - Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation); - +public interface JsonValidator extends JsonSchemaValidator { /** * In case the {@link com.networknt.schema.JsonValidator} has a related {@link com.networknt.schema.JsonSchema} or several * ones, calling preloadJsonSchema will actually load the schema document(s) eagerly. @@ -52,40 +32,6 @@ default void preloadJsonSchema() throws JsonSchemaException { // do nothing by default - to be overridden in subclasses } - /** - * This is default implementation of walk method. Its job is to call the - * validate method if shouldValidateSchema is enabled. - */ - @Override - default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - if (node == null) { - // Note that null is not the same as NullNode - return Collections.emptySet(); - } - return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) - : Collections.emptySet(); - } - - /** - * The schema location is the canonical URI of the schema object plus a JSON - * Pointer fragment indicating the subschema that produced a result. In contrast - * with the evaluation path, the schema location MUST NOT include by-reference - * applicators such as $ref or $dynamicRef. - * - * @return the schema location - */ - SchemaLocation getSchemaLocation(); - - /** - * The evaluation path is the set of keys, starting from the schema root, - * through which evaluation passes to reach the schema object that produced a - * specific result. - * - * @return the evaluation path - */ - JsonNodePath getEvaluationPath(); - /** * The keyword of the validator. * diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 3dd0e32f3..07806b3cf 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -31,7 +31,7 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnionTypeValidator.class); - private final List schemas; + private final List schemas; private final String error; public UnionTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { @@ -78,7 +78,7 @@ public Set validate(ExecutionContext executionContext, JsonNo boolean failFast = executionContext.isFailFast(); try { executionContext.setFailFast(false); - for (JsonValidator schema : schemas) { + for (JsonSchemaValidator schema : schemas) { Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); if (errors == null || errors.isEmpty()) { valid = true; @@ -103,8 +103,12 @@ public Set validate(ExecutionContext executionContext, JsonNo @Override public void preloadJsonSchema() { - for (final JsonValidator validator : schemas) { - validator.preloadJsonSchema(); + for (final JsonSchemaValidator validator : schemas) { + if (validator instanceof JsonValidator) { + ((JsonValidator) validator).preloadJsonSchema(); + } else if (validator instanceof JsonSchema) { + ((JsonSchema) validator).initializeValidators(); + } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java deleted file mode 100644 index 819153bfe..000000000 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ /dev/null @@ -1,131 +0,0 @@ -package com.networknt.schema; - -import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.i18n.MessageSource; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; - -/** - * Validation message handler. - */ -public abstract class ValidationMessageHandler { - protected final ErrorMessageType errorMessageType; - protected final String errorMessageKeyword; - protected final MessageSource messageSource; - protected final Keyword keyword; - protected final JsonSchema parentSchema; - protected final SchemaLocation schemaLocation; - protected final JsonNodePath evaluationPath; - protected final JsonSchema evaluationParentSchema; - protected final Map errorMessage; - - protected ValidationMessageHandler(ErrorMessageType errorMessageType, String errorMessageKeyword, - MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, - JsonNodePath evaluationPath) { - this.errorMessageType = errorMessageType; - this.messageSource = messageSource; - this.schemaLocation = Objects.requireNonNull(schemaLocation); - this.evaluationPath = Objects.requireNonNull(evaluationPath); - this.parentSchema = parentSchema; - this.evaluationParentSchema = null; - this.errorMessageKeyword = errorMessageKeyword; - this.keyword = keyword; - - Map currentErrorMessage = null; - if (this.keyword != null) { - if (this.errorMessageKeyword != null && keyword != null && parentSchema != null) { - currentErrorMessage = getErrorMessage(this.errorMessageKeyword, parentSchema.getSchemaNode(), - keyword.getValue()); - } - } - this.errorMessage = currentErrorMessage; - } - - /** - * Constructor to create a copy using fields. - * - * @param errorMessageType the error message type - * @param errorMessageKeyword the error message keyword - * @param messageSource the message source - * @param keyword the keyword - * @param parentSchema the parent schema - * @param schemaLocation the schema location - * @param evaluationPath the evaluation path - * @param evaluationParentSchema the evaluation parent schema - * @param errorMessage the error message - */ - protected ValidationMessageHandler(ErrorMessageType errorMessageType, String errorMessageKeyword, - MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, - JsonNodePath evaluationPath, JsonSchema evaluationParentSchema, Map errorMessage) { - this.errorMessageType = errorMessageType; - this.errorMessageKeyword = errorMessageKeyword; - this.messageSource = messageSource; - this.keyword = keyword; - this.parentSchema = parentSchema; - this.schemaLocation = schemaLocation; - this.evaluationPath = evaluationPath; - this.evaluationParentSchema = evaluationParentSchema; - this.errorMessage = errorMessage; - } - - protected MessageSourceValidationMessage.Builder message() { - return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, (message, failFast) -> { - if (failFast) { - throw new FailFastAssertionException(message); - } - }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) - .evaluationPath(this.evaluationPath).type(this.keyword != null ? this.keyword.getValue() : null) - .messageKey(getErrorMessageType().getErrorCodeValue()); - } - - protected ErrorMessageType getErrorMessageType() { - return this.errorMessageType; - } - - /** - * Gets the custom error message to use. - * @param errorMessageKeyword the error message keyword - * @param schemaNode the schema node - * @param keyword the keyword - * @return the custom error message - */ - protected Map getErrorMessage(String errorMessageKeyword, JsonNode schemaNode, String keyword) { - final JsonSchema parentSchema = this.parentSchema; - final JsonNode message = getMessageNode(errorMessageKeyword, schemaNode, parentSchema, keyword); - if (message != null) { - JsonNode messageNode = message.get(keyword); - 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 Collections.emptyMap(); - } - - protected JsonNode getMessageNode(String errorMessageKeyword, JsonNode schemaNode, JsonSchema parentSchema, - String pname) { - if (schemaNode.get(errorMessageKeyword) != null && schemaNode.get(errorMessageKeyword).get(pname) != null) { - return schemaNode.get(errorMessageKeyword); - } - JsonNode messageNode; - messageNode = schemaNode.get(errorMessageKeyword); - if (messageNode == null && parentSchema != null) { - messageNode = parentSchema.schemaNode.get(errorMessageKeyword); - if (messageNode == null) { - return getMessageNode(errorMessageKeyword, parentSchema.schemaNode, parentSchema.getParentSchema(), - pname); - } - } - return messageNode; - } -} diff --git a/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java index f2665bbfc..cda3a3be7 100644 --- a/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java +++ b/src/main/java/com/networknt/schema/format/BaseFormatJsonValidator.java @@ -19,7 +19,7 @@ public abstract class BaseFormatJsonValidator extends BaseJsonValidator { public BaseFormatJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, ValidationContext validationContext) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, errorMessageType, keyword, validationContext, false); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, errorMessageType, keyword, validationContext); VersionFlag dialect = this.validationContext.getMetaSchema().getSpecification(); if (dialect == null || dialect.getVersionFlagValue() < VersionFlag.V201909.getVersionFlagValue()) { assertionsEnabled = true; diff --git a/src/test/java/com/networknt/schema/Issue824Test.java b/src/test/java/com/networknt/schema/Issue824Test.java index ef4ee4ba2..5367f471b 100644 --- a/src/test/java/com/networknt/schema/Issue824Test.java +++ b/src/test/java/com/networknt/schema/Issue824Test.java @@ -19,7 +19,6 @@ void validate() throws JsonProcessingException { schemaMappers.mapPrefix("https://json-schema.org", "resource:"); }).build() .getSchema(SchemaLocation.of(JsonMetaSchema.getV201909().getIri())); - v201909SpecSchema.preloadJsonSchema(); final JsonNode invalidSchema = new ObjectMapper().readTree( "{"+ " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\","+ diff --git a/src/test/java/com/networknt/schema/MessageTest.java b/src/test/java/com/networknt/schema/MessageTest.java index 3c270e894..095d72c8b 100644 --- a/src/test/java/com/networknt/schema/MessageTest.java +++ b/src/test/java/com/networknt/schema/MessageTest.java @@ -36,9 +36,8 @@ static class EqualsValidator extends BaseJsonValidator { EqualsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, Keyword keyword, - ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, ERROR_MESSAGE_TYPE, keyword, validationContext, - suppressSubSchemaRetrieval); + ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ERROR_MESSAGE_TYPE, keyword, validationContext); this.value = schemaNode.textValue(); } @@ -66,7 +65,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { - return new EqualsValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, this, validationContext, false); + return new EqualsValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, this, validationContext); } }