diff --git a/doc/walkers.md b/doc/walkers.md index 3813ed62a..b06fb120f 100644 --- a/doc/walkers.md +++ b/doc/walkers.md @@ -17,18 +17,20 @@ public interface JsonSchemaWalker { * cutting concerns like logging or instrumentation. This method also performs * the validation if {@code shouldValidateSchema} is set to true.
*
- * {@link BaseJsonValidator#walk(JsonNode, JsonNode, String, boolean)} provides - * a default implementation of this method. However keywords that parse + * {@link BaseJsonValidator#walk(ExecutionContext, JsonNode, JsonNode, JsonNodePath, boolean)} provides + * a default implementation of this method. However validators that parse * sub-schemas should override this method to call walk method on those - * subschemas. + * sub-schemas. * + * @param executionContext ExecutionContext * @param node JsonNode * @param rootNode JsonNode - * @param at String + * @param instanceLocation JsonNodePath * @param shouldValidateSchema boolean * @return a set of validation messages if shouldValidateSchema is true. */ - Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema); + Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema); } ``` @@ -41,10 +43,11 @@ The JSONValidator interface extends this new interface thus allowing all the val * validate method if shouldValidateSchema is enabled. */ @Override - public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema); Set validationMessages = new LinkedHashSet(); if (shouldValidateSchema) { - validationMessages = validate(node, rootNode, at); + validationMessages = validate(executionContext, node, rootNode, instanceLocation); } return validationMessages; } @@ -54,7 +57,7 @@ The JSONValidator interface extends this new interface thus allowing all the val A new walk method added to the JSONSchema class allows us to walk through the JSONSchema. ```java - public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { + public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { // Create the collector context object. CollectorContext collectorContext = new CollectorContext(); // Set the collector context in thread info, this is unique for every thread. @@ -66,7 +69,7 @@ A new walk method added to the JSONSchema class allows us to walk through the JS ValidationResult validationResult = new ValidationResult(errors, collectorContext); return validationResult; } - + @Override public Set walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { Set validationMessages = new LinkedHashSet(); @@ -174,14 +177,15 @@ Following snippet shows the details captured by WalkEvent instance. ```java public class WalkEvent { - private String schemaPath; + private ExecutionContext executionContext; + private SchemaLocation schemaLocation; + private JsonNodePath evaluationPath; private JsonNode schemaNode; private JsonSchema parentSchema; - private String keyWordName; + private String keyword; private JsonNode node; private JsonNode rootNode; - private String at; - + private JsonNodePath instanceLocation; ``` ### Sample Flow diff --git a/src/main/java/com/networknt/schema/AbsoluteIri.java b/src/main/java/com/networknt/schema/AbsoluteIri.java new file mode 100644 index 000000000..ebfdb419d --- /dev/null +++ b/src/main/java/com/networknt/schema/AbsoluteIri.java @@ -0,0 +1,179 @@ +/* + * 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.Objects; + +/** + * The absolute IRI is an IRI without the fragment. + *

+ * absolute-IRI = scheme ":" ihier-part [ "?" iquery ] + *

+ * This does not attempt to validate whether the value really conforms to an + * absolute IRI format as in earlier drafts the IDs are not defined as such. + */ +public class AbsoluteIri { + private final String value; + + /** + * Constructs a new IRI given the value. + * + * @param value + */ + public AbsoluteIri(String value) { + this.value = value; + } + + /** + * Constructs a new IRI given the value. + * + * @param iri the value + * @return the new absolute IRI + */ + public static AbsoluteIri of(String iri) { + return new AbsoluteIri(iri); + } + + /** + * Constructs a new IRI by parsing the given string and then resolving it + * against this IRI. + * + * @param iri to resolve + * @return the new absolute IRI + */ + public AbsoluteIri resolve(String iri) { + return new AbsoluteIri(resolve(this.value, iri)); + } + + /** + * Gets the scheme of the IRI. + * + * @return the scheme + */ + public String getScheme() { + return getScheme(this.value); + } + + /** + * Returns the scheme and authority components of the IRI. + * + * @return the scheme and authority components + */ + protected String getSchemeAuthority() { + return getSchemeAuthority(this.value); + } + + /** + * Constructs a new IRI by parsing the given string and then resolving it + * against this IRI. + * + * @param parent the parent absolute IRI + * @param iri to resolve + * @return the new absolute IRI + */ + public static String resolve(String parent, String iri) { + if (iri.contains(":")) { + // IRI is absolute + return iri; + } else { + // IRI is relative to this + if (parent == null) { + return iri; + } + if (iri.startsWith("/")) { + // IRI is relative to this root + return getSchemeAuthority(parent) + iri; + } else { + // IRI is relative to this path + String base = parent; + int scheme = parent.indexOf("://"); + if (scheme == -1) { + scheme = 0; + } else { + scheme = scheme + 3; + } + int slash = parent.lastIndexOf('/'); + if (slash != -1 && slash > scheme) { + base = parent.substring(0, slash); + } + return base + "/" + iri; + } + } + } + + /** + * Returns the scheme and authority components of the IRI. + * + * @param iri to parse + * @return the scheme and authority components + */ + protected static String getSchemeAuthority(String iri) { + if (iri == null) { + return ""; + } + // iri refers to root + int start = iri.indexOf("://"); + if (start == -1) { + start = 0; + } else { + start = start + 3; + } + int end = iri.indexOf('/', start); + return end != -1 ? iri.substring(0, end) : iri; + } + + /** + * Returns the scheme of the IRI. + * + * @param iri to parse + * @return the scheme + */ + public static String getScheme(String iri) { + if (iri == null) { + return ""; + } + // iri refers to root + int start = iri.indexOf(":"); + if (start == -1) { + return ""; + } else { + return iri.substring(0, start); + } + } + + @Override + public String toString() { + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(this.value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + AbsoluteIri other = (AbsoluteIri) obj; + return Objects.equals(this.value, other.value); + } + +} diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index f87a6d806..75f84623b 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -16,23 +16,34 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; +public abstract class AbstractJsonValidator implements JsonValidator { + private final SchemaLocation schemaLocation; + private final JsonNodePath evaluationPath; + private final Keyword keyword; -import java.util.Collections; -import java.util.Set; + public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword) { + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.keyword = keyword; + } -public abstract class AbstractJsonValidator implements JsonValidator { + @Override + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } - public Set validate(ExecutionContext executionContext, JsonNode node) { - return validate(executionContext, node, node, PathType.LEGACY.getRoot()); + @Override + public JsonNodePath getEvaluationPath() { + return evaluationPath; } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - Set validationMessages = Collections.emptySet(); - if (shouldValidateSchema) { - validationMessages = validate(executionContext, node, rootNode, at); - } - return validationMessages; - } + public String getKeyword() { + return keyword.getValue(); + } + + @Override + public String toString() { + return getEvaluationPath().getName(-1); + } } diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 42b489095..42994d3b1 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -32,26 +32,28 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator { private final Set allowedProperties; private final List patternProperties = new ArrayList<>(); - public AdditionalPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + public AdditionalPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES, validationContext); if (schemaNode.isBoolean()) { allowAdditionalProperties = schemaNode.booleanValue(); additionalPropertiesSchema = null; } else if (schemaNode.isObject()) { allowAdditionalProperties = true; - additionalPropertiesSchema = validationContext.newSchema(getValidatorType().getValue(), schemaNode, parentSchema); + additionalPropertiesSchema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { allowAdditionalProperties = false; additionalPropertiesSchema = null; } - allowedProperties = new HashSet(); JsonNode propertiesNode = parentSchema.getSchemaNode().get(PropertiesValidator.PROPERTY); if (propertiesNode != null) { + allowedProperties = new HashSet<>(); for (Iterator it = propertiesNode.fieldNames(); it.hasNext(); ) { allowedProperties.add(it.next()); } + } else { + allowedProperties = Collections.emptySet(); } JsonNode patternPropertiesNode = parentSchema.getSchemaNode().get(PatternPropertiesValidator.PROPERTY); @@ -60,27 +62,27 @@ public AdditionalPropertiesValidator(String schemaPath, JsonNode schemaNode, Jso patternProperties.add(RegularExpression.compile(it.next(), validationContext)); } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = new LinkedHashSet(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!node.isObject()) { // ignore no object - return errors; + return Collections.emptySet(); } - // if allowAdditionalProperties is true, add all the properties as evaluated. - if (allowAdditionalProperties) { - for (Iterator it = node.fieldNames(); it.hasNext(); ) { - collectorContext.getEvaluatedProperties().add(atPath(at, it.next())); + CollectorContext collectorContext = executionContext.getCollectorContext(); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + // if allowAdditionalProperties is true, add all the properties as evaluated. + if (allowAdditionalProperties) { + for (Iterator it = node.fieldNames(); it.hasNext(); ) { + collectorContext.getEvaluatedProperties().add(instanceLocation.append(it.next())); + } } } + Set errors = null; + for (Iterator it = node.fieldNames(); it.hasNext(); ) { String pname = it.next(); // skip the context items @@ -97,26 +99,42 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!allowedProperties.contains(pname) && !handledByPatternProperties) { if (!allowAdditionalProperties) { - errors.add(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), pname)); + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.add(message().property(pname).instanceLocation(instanceLocation.append(pname)) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(pname).build()); } else { if (additionalPropertiesSchema != null) { - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); if (state != null && state.isWalkEnabled()) { - errors.addAll(additionalPropertiesSchema.walk(executionContext, node.get(pname), rootNode, atPath(at, pname), state.isValidationEnabled())); + Set results = additionalPropertiesSchema.walk(executionContext, node.get(pname), rootNode, instanceLocation.append(pname), state.isValidationEnabled()); + if (!results.isEmpty()) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(results); + } } else { - errors.addAll(additionalPropertiesSchema.validate(executionContext, node.get(pname), rootNode, atPath(at, pname))); + Set results = additionalPropertiesSchema.validate(executionContext, node.get(pname), rootNode, instanceLocation.append(pname)); + if (!results.isEmpty()) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(results); + } } } } } } - return Collections.unmodifiableSet(errors); + return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } if (node == null || !node.isObject()) { @@ -142,9 +160,9 @@ public Set walk(ExecutionContext executionContext, JsonNode n if (!allowedProperties.contains(pname) && !handledByPatternProperties) { if (allowAdditionalProperties) { if (additionalPropertiesSchema != null) { - ValidatorState state = (ValidatorState) executionContext.getCollectorContext().get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); if (state != null && state.isWalkEnabled()) { - additionalPropertiesSchema.walk(executionContext, node.get(pname), rootNode, atPath(at, pname), state.isValidationEnabled()); + additionalPropertiesSchema.walk(executionContext, node.get(pname), rootNode, instanceLocation.append(pname), state.isValidationEnabled()); } } } diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 294c1fb8b..30074a548 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -30,22 +30,23 @@ public class AllOfValidator extends BaseJsonValidator { private final List schemas = new ArrayList<>(); - public AllOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext); + public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext); this.validationContext = validationContext; int size = schemaNode.size(); for (int i = 0; i < size; i++) { - this.schemas.add(validationContext.newSchema(schemaPath + "/" + i, schemaNode.get(i), parentSchema)); + this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), + schemaNode.get(i), parentSchema)); } } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); Set childSchemaErrors = new LinkedHashSet<>(); @@ -55,9 +56,9 @@ public Set validate(ExecutionContext executionContext, JsonNo Scope parentScope = collectorContext.enterDynamicScope(); try { if (!state.isWalkEnabled()) { - localErrors = schema.validate(executionContext, node, rootNode, at); + localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); } else { - localErrors = schema.walk(executionContext, node, rootNode, at, true); + localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); } childSchemaErrors.addAll(localErrors); @@ -74,7 +75,7 @@ public Set validate(ExecutionContext executionContext, JsonNo final ObjectNode discriminator = currentDiscriminatorContext .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); if (null != discriminator) { - registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, this.parentSchema, at); + 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(); final JsonNode discriminatorNode = node.get(discriminatorPropertyName); @@ -105,13 +106,13 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } for (JsonSchema schema : this.schemas) { // Walk through the schema - schema.walk(executionContext, node, rootNode, at, false); + schema.walk(executionContext, node, rootNode, instanceLocation, false); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/Annotations.java b/src/main/java/com/networknt/schema/Annotations.java new file mode 100644 index 000000000..b959f86ba --- /dev/null +++ b/src/main/java/com/networknt/schema/Annotations.java @@ -0,0 +1,98 @@ +/* + * 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.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Annotations. + */ +public class Annotations { + public static final Set UNEVALUATED_PROPERTIES_ANNOTATIONS; + public static final Set UNEVALUATED_ITEMS_ANNOTATIONS; + public static final Set EVALUATION_ANNOTATIONS; + + public static final Predicate UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; + public static final Predicate UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; + public static final Predicate EVALUATION_ANNOTATIONS_PREDICATE; + public static final Predicate PREDICATE_FALSE; + + static { + Set unevaluatedProperties = new HashSet<>(); + unevaluatedProperties.add("unevaluatedProperties"); + unevaluatedProperties.add("properties"); + unevaluatedProperties.add("patternProperties"); + unevaluatedProperties.add("additionalProperties"); + UNEVALUATED_PROPERTIES_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedProperties); + + Set unevaluatedItems = new HashSet<>(); + unevaluatedItems.add("unevaluatedItems"); + unevaluatedItems.add("items"); + unevaluatedItems.add("prefixItems"); + unevaluatedItems.add("additionalItems"); + unevaluatedItems.add("contains"); + UNEVALUATED_ITEMS_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedItems); + + Set evaluation = new HashSet<>(); + evaluation.addAll(unevaluatedProperties); + evaluation.addAll(unevaluatedItems); + EVALUATION_ANNOTATIONS = Collections.unmodifiableSet(evaluation); + + UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE = UNEVALUATED_PROPERTIES_ANNOTATIONS::contains; + UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE = UNEVALUATED_ITEMS_ANNOTATIONS::contains; + EVALUATION_ANNOTATIONS_PREDICATE = EVALUATION_ANNOTATIONS::contains; + PREDICATE_FALSE = (keyword) -> false; + } + + /** + * Gets the default annotation allow list. + * + * @param metaSchema the meta schema + */ + public static Set getDefaultAnnotationAllowList(JsonMetaSchema metaSchema) { + boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; + boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; + if (unevaluatedProperties && unevaluatedItems) { + return EVALUATION_ANNOTATIONS; + } else if (unevaluatedProperties && !unevaluatedItems) { + return UNEVALUATED_PROPERTIES_ANNOTATIONS; + } else if (!unevaluatedProperties && unevaluatedItems) { + return UNEVALUATED_ITEMS_ANNOTATIONS; + } + return Collections.emptySet(); + } + + /** + * Gets the default annotation allow list predicate. + * + * @param metaSchema the meta schema + */ + public static Predicate getDefaultAnnotationAllowListPredicate(JsonMetaSchema metaSchema) { + boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; + boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; + if (unevaluatedProperties && unevaluatedItems) { + return EVALUATION_ANNOTATIONS_PREDICATE; + } else if (unevaluatedProperties && !unevaluatedItems) { + return UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; + } else if (!unevaluatedProperties && unevaluatedItems) { + return UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; + } + return PREDICATE_FALSE; + } +} diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index bffac5e13..99ffb905a 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -32,12 +32,13 @@ public class AnyOfValidator extends BaseJsonValidator { private final List schemas = new ArrayList<>(); private final ValidationContext.DiscriminatorContext discriminatorContext; - public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); + public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); this.validationContext = validationContext; int size = schemaNode.size(); for (int i = 0; i < size; i++) { - this.schemas.add(validationContext.newSchema(schemaPath + "/" + i, schemaNode.get(i), parentSchema)); + this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), + schemaNode.get(i), parentSchema)); } if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { @@ -48,15 +49,15 @@ public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - this.validationContext.enterDiscriminatorContext(this.discriminatorContext, at); + this.validationContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation); } boolean initialHasMatchedNode = state.hasMatchedNode(); @@ -72,20 +73,20 @@ public Set validate(ExecutionContext executionContext, JsonNo try { state.setMatchedNode(initialHasMatchedNode); - if (schema.hasTypeValidator()) { - TypeValidator typeValidator = schema.getTypeValidator(); + TypeValidator typeValidator = schema.getTypeValidator(); + if (typeValidator != null) { //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(null, at, - executionContext.getExecutionConfig().getLocale(), typeValidator.getSchemaType().toString())); + allErrors + .addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); continue; } } if (!state.isWalkEnabled()) { - errors = schema.validate(executionContext, node, rootNode, at); + errors = schema.validate(executionContext, node, rootNode, instanceLocation); } else { - errors = schema.walk(executionContext, node, rootNode, at, true); + errors = schema.walk(executionContext, node, rootNode, instanceLocation, true); } // check if any validation errors have occurred @@ -107,8 +108,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(buildValidationMessage(null, - at, executionContext.getExecutionConfig().getLocale(), DISCRIMINATOR_REMARK)); + allErrors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(DISCRIMINATOR_REMARK).build()); } else { // Clear all errors. allErrors.clear(); @@ -126,7 +128,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } // determine only those errors which are NOT of type "required" property missing - Set childNotRequiredErrors = allErrors.stream().filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())).collect(Collectors.toSet()); + Set childNotRequiredErrors = allErrors.stream() + .filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())) + .collect(Collectors.toCollection(LinkedHashSet::new)); // in case we had at least one (anyOf, i.e. any number >= 1 of) valid subschemas, we can remove all other errors about "required" properties if (numberOfValidSubSchemas >= 1 && childNotRequiredErrors.isEmpty()) { @@ -134,14 +138,17 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { - final Set errors = new HashSet<>(); - errors.add(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), "based on the provided discriminator. No alternative could be chosen based on the discriminator property")); + final Set errors = new LinkedHashSet<>(); + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments( + "based on the provided discriminator. No alternative could be chosen based on the discriminator property") + .build()); return Collections.unmodifiableSet(errors); } } finally { if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - this.validationContext.leaveDiscriminatorContextImmediately(at); + this.validationContext.leaveDiscriminatorContextImmediately(instanceLocation); } Scope parentScope = collectorContext.exitDynamicScope(); @@ -154,12 +161,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } for (JsonSchema schema : this.schemas) { - schema.walk(executionContext, node, rootNode, at, false); + schema.walk(executionContext, node, rootNode, instanceLocation, false); } return new LinkedHashSet<>(); } diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 478089911..6de3bf544 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -38,22 +38,35 @@ public abstract class BaseJsonValidator extends ValidationMessageHandler impleme protected ValidationContext validationContext; - public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidatorTypeCode validatorType, ValidationContext validationContext) { - this(schemaPath, schemaNode, parentSchema, validatorType, validationContext, false); + public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidatorTypeCode validatorType, ValidationContext validationContext) { + this(schemaLocation, evaluationPath, schemaNode, parentSchema, validatorType, validatorType, validationContext, + false); } - public BaseJsonValidator(String schemaPath, - JsonNode schemaNode, - JsonSchema parentSchema, - 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().getMessageSource() : DefaultMessageSource.getInstance(), validatorType, parentSchema, schemaPath); + public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, + ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { + super(validationContext != null + && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), + errorMessageType, + (validationContext != null && validationContext.getConfig() != null) + ? validationContext.getConfig().isCustomMessageSupported() + : true, + (validationContext != null && validationContext.getConfig() != null) + ? validationContext.getConfig().getMessageSource() + : DefaultMessageSource.getInstance(), + keyword, parentSchema, schemaLocation, evaluationPath); + this.validationContext = validationContext; 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; - this.pathType = (validationContext != null && validationContext.getConfig() != null && validationContext.getConfig().getPathType() != null) ? validationContext.getConfig().getPathType() : PathType.DEFAULT; + this.applyDefaultsStrategy = (validationContext != null && validationContext.getConfig() != null + && validationContext.getConfig().getApplyDefaultsStrategy() != null) + ? validationContext.getConfig().getApplyDefaultsStrategy() + : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY; + this.pathType = (validationContext != null && validationContext.getConfig() != null + && validationContext.getConfig().getPathType() != null) ? validationContext.getConfig().getPathType() + : PathType.DEFAULT; } private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) { @@ -86,8 +99,8 @@ protected static boolean equals(double n1, double n2) { return Math.abs(n1 - n2) < 1e-12; } - protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, String at) { - logger.debug("validate( {}, {}, {})", node, rootNode, at); + protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + logger.debug("validate( {}, {}, {})", node, rootNode, instanceLocation); } /** @@ -134,19 +147,19 @@ && noExplicitDiscriminatorKeyOverride(discriminatorMapping, jsonSchema)) { * @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 at the logging prefix + * @param instanceLocation the logging prefix */ protected static void registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext, final ObjectNode discriminator, final JsonSchema schema, - final String at) { + final JsonNodePath instanceLocation) { final JsonNode discriminatorOnSchema = schema.schemaNode.get("discriminator"); if (null != discriminatorOnSchema && null != currentDiscriminatorContext - .getDiscriminatorForPath(schema.schemaPath)) { + .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(at + " schema " + schema + " attempts redefining the discriminator property"); + 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"); @@ -165,7 +178,7 @@ protected static void registerAndMergeDiscriminator(final DiscriminatorContext c final JsonNode currentMappingValue = mappingOnContextDiscriminator.get(mappingKeyToAdd); if (null != currentMappingValue && currentMappingValue != mappingValueToAdd) { - throw new JsonSchemaException(at + "discriminator mapping redefinition from " + mappingKeyToAdd + throw new JsonSchemaException(instanceLocation + "discriminator mapping redefinition from " + mappingKeyToAdd + "/" + currentMappingValue + " to " + mappingValueToAdd); } else if (null == currentMappingValue) { mappingOnContextDiscriminator.set(mappingKeyToAdd, mappingValueToAdd); @@ -173,13 +186,13 @@ protected static void registerAndMergeDiscriminator(final DiscriminatorContext c } } } - currentDiscriminatorContext.registerDiscriminator(schema.schemaPath, discriminator); + currentDiscriminatorContext.registerDiscriminator(schema.schemaLocation, discriminator); } private static void checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext, final String discriminatorPropertyValue, final JsonSchema schema) { - if (schema.schemaPath.endsWith("/" + discriminatorPropertyValue)) { + if (schema.schemaLocation.getFragment().getName(-1).equals(discriminatorPropertyValue)) { currentDiscriminatorContext.markMatch(); } } @@ -192,7 +205,8 @@ private static void checkForExplicitDiscriminatorMappingMatch(final Discriminato while (explicitMappings.hasNext()) { final Map.Entry candidateExplicitMapping = explicitMappings.next(); if (candidateExplicitMapping.getKey().equals(discriminatorPropertyValue) - && schema.schemaPath.equals(candidateExplicitMapping.getValue().asText())) { + && ("#" + schema.schemaLocation.getFragment().toString()) + .equals(candidateExplicitMapping.getValue().asText())) { currentDiscriminatorContext.markMatch(); break; } @@ -204,15 +218,27 @@ private static boolean noExplicitDiscriminatorKeyOverride(final JsonNode discrim final Iterator> explicitMappings = discriminatorMapping.fields(); while (explicitMappings.hasNext()) { final Map.Entry candidateExplicitMapping = explicitMappings.next(); - if (candidateExplicitMapping.getValue().asText().equals(parentSchema.schemaPath)) { + if (candidateExplicitMapping.getValue().asText() + .equals(parentSchema.schemaLocation.getFragment().toString())) { return false; } } return true; } - public String getSchemaPath() { - return this.schemaPath; + @Override + public SchemaLocation getSchemaLocation() { + return this.schemaLocation; + } + + @Override + public JsonNodePath getEvaluationPath() { + return this.evaluationPath; + } + + @Override + public String getKeyword() { + return this.keyword.getValue(); } public JsonNode getSchemaNode() { @@ -227,11 +253,43 @@ protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) { return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext); } - @Override public Set validate(ExecutionContext executionContext, JsonNode node) { return validate(executionContext, node, node, atRoot()); } + /** + * Validates to a format. + * + * @param the result type + * @param executionContext the execution context + * @param node the node + * @param format the format + * @return the result + */ + public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format) { + return validate(executionContext, node, format, null); + } + + /** + * Validates to a format. + * + * @param the result type + * @param executionContext the execution context + * @param node the node + * @param format the format + * @param executionCustomizer the customizer + * @return the result + */ + public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format, + ExecutionCustomizer executionCustomizer) { + format.customize(executionContext, this.validationContext); + if (executionCustomizer != null) { + executionCustomizer.customize(executionContext, validationContext); + } + Set validationMessages = validate(executionContext, node); + return format.format(validationMessages, executionContext, this.validationContext); + } + protected String getNodeFieldType() { JsonNode typeField = this.getParentSchema().getSchemaNode().get("type"); if (typeField != null) { @@ -246,39 +304,17 @@ protected void preloadJsonSchemas(final Collection schemas) { } } - - protected PathType getPathType() { - return this.pathType; - } - /** * Get the root path. * * @return The path. */ - protected String atRoot() { - return this.pathType.getRoot(); - } - - /** - * Create the path for a given child token. - * - * @param currentPath The current path. - * @param token The child token. - * @return The complete path. - */ - protected String atPath(String currentPath, String token) { - return this.pathType.append(currentPath, token); + protected JsonNodePath atRoot() { + return new JsonNodePath(this.pathType); } - /** - * Create the path for a given child indexed item. - * - * @param currentPath The current path. - * @param index The child index. - * @return The complete path. - */ - protected String atPath(String currentPath, int index) { - return this.pathType.append(currentPath, index); + @Override + public String toString() { + return getEvaluationPath().getName(-1); } } diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 66157f171..36647ef41 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -119,7 +119,7 @@ public JsonSchema getOutermostSchema() { * * @return the set of evaluated items (never null) */ - public Collection getEvaluatedItems() { + public Collection getEvaluatedItems() { return getDynamicScope().getEvaluatedItems(); } @@ -128,7 +128,7 @@ public Collection getEvaluatedItems() { * * @return the set of evaluated properties (never null) */ - public Collection getEvaluatedProperties() { + public Collection getEvaluatedProperties() { return getDynamicScope().getEvaluatedProperties(); } @@ -177,6 +177,15 @@ public Object get(String name) { return this.collectorMap.get(name); } + /** + * Gets the collector map. + * + * @return the collector map + */ + public Map getCollectorMap() { + return this.collectorMap; + } + /** * Returns all the collected data. Please look into {@link #get(String)} method for more details. * @return Map @@ -231,12 +240,12 @@ public static class Scope { /** * Used to track which array items have been evaluated. */ - private final Collection evaluatedItems; + private final Collection evaluatedItems; /** * Used to track which properties have been evaluated. */ - private final Collection evaluatedProperties; + private final Collection evaluatedProperties; private final boolean top; @@ -251,16 +260,16 @@ public static class Scope { this.evaluatedProperties = newCollection(disableUnevaluatedProperties); } - private static Collection newCollection(boolean disabled) { - return !disabled ? new ArrayList<>() : new AbstractCollection() { + private static Collection newCollection(boolean disabled) { + return !disabled ? new ArrayList<>() : new AbstractCollection() { @Override - public boolean add(String e) { + public boolean add(JsonNodePath e) { return false; } @Override - public Iterator iterator() { + public Iterator iterator() { return Collections.emptyIterator(); } @@ -290,7 +299,7 @@ public JsonSchema getContainingSchema() { * * @return the set of evaluated items (never null) */ - public Collection getEvaluatedItems() { + public Collection getEvaluatedItems() { return this.evaluatedItems; } @@ -299,7 +308,7 @@ public Collection getEvaluatedItems() { * * @return the set of evaluated properties (never null) */ - public Collection getEvaluatedProperties() { + public Collection getEvaluatedProperties() { return this.evaluatedProperties; } @@ -309,8 +318,12 @@ public Collection getEvaluatedProperties() { * @return this scope */ public Scope mergeWith(Scope scope) { - getEvaluatedItems().addAll(scope.getEvaluatedItems()); - getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); + if (!scope.getEvaluatedItems().isEmpty()) { + getEvaluatedItems().addAll(scope.getEvaluatedItems()); + } + if (!scope.getEvaluatedProperties().isEmpty()) { + getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); + } return this; } diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index df47af289..7c50dbab3 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -26,22 +26,23 @@ public class ConstValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(ConstValidator.class); JsonNode schemaNode; - public ConstValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.CONST, validationContext); + public ConstValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONST, validationContext); this.schemaNode = schemaNode; } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - return Collections.singleton(buildValidationMessage(null, - at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()) + .build()); } } else if (!schemaNode.equals(node)) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), schemaNode.asText())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 5a147c914..fb0268187 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -41,8 +41,8 @@ public class ContainsValidator extends BaseJsonValidator { private int min = 1; private int max = Integer.MAX_VALUE; - public ContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext); + public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext); // Draft 6 added the contains keyword but maxContains and minContains first // appeared in Draft 2019-09 so the semantics of the validation changes @@ -50,8 +50,7 @@ public ContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare isMinV201909 = MinV201909.getVersions().contains(SpecVersionDetector.detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION)); if (schemaNode.isObject() || schemaNode.isBoolean()) { - this.schema = validationContext.newSchema(getValidatorType().getValue(), schemaNode, parentSchema); - + this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); JsonNode parentSchemaNode = parentSchema.getSchemaNode(); Optional.ofNullable(parentSchemaNode.get(ValidatorTypeCode.MAX_CONTAINS.getValue())) .filter(JsonNode::canConvertToExactIntegral) @@ -63,25 +62,25 @@ public ContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema pare } else { this.schema = null; } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); // ignores non-arrays if (null != this.schema && node.isArray()) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); int actual = 0, i = 0; for (JsonNode n : node) { - String path = atPath(at, i); + JsonNodePath path = instanceLocation.append(i); if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { ++actual; - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } ++i; } @@ -91,7 +90,7 @@ public Set validate(ExecutionContext executionContext, JsonNo updateValidatorType(ValidatorTypeCode.MIN_CONTAINS); } return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), - executionContext.getExecutionConfig().getLocale(), at, this.min); + executionContext.getExecutionConfig().getLocale(), instanceLocation, this.min); } if (actual > this.max) { @@ -99,7 +98,7 @@ public Set validate(ExecutionContext executionContext, JsonNo updateValidatorType(ValidatorTypeCode.MAX_CONTAINS); } return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), - executionContext.getExecutionConfig().getLocale(), at, this.max); + executionContext.getExecutionConfig().getLocale(), instanceLocation, this.max); } } @@ -111,7 +110,8 @@ public void preloadJsonSchema() { Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators); } - private Set boundsViolated(String messageKey, Locale locale, String at, int bounds) { - return Collections.singleton(buildValidationMessage(null, at, messageKey, locale, String.valueOf(bounds), this.schema.getSchemaNode().toString())); + private Set boundsViolated(String messageKey, Locale locale, JsonNodePath instanceLocation, int bounds) { + return Collections.singleton(message().instanceLocation(instanceLocation).messageKey(messageKey).locale(locale) + .arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()).build()); } } diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 93b1b007f..1415ab9f3 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -27,9 +27,9 @@ public class DependenciesValidator extends BaseJsonValidator implements JsonVali private final Map> propertyDeps = new HashMap>(); private final Map schemaDeps = new HashMap(); - public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public DependenciesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES, validationContext); for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); @@ -44,15 +44,14 @@ public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema depsProps.add(pvalue.get(i).asText()); } } else if (pvalue.isObject() || pvalue.isBoolean()) { - schemaDeps.put(pname, validationContext.newSchema(pname, pvalue, parentSchema)); + schemaDeps.put(pname, validationContext.newSchema(schemaLocation.append(pname), + evaluationPath.append(pname), pvalue, parentSchema)); } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); Set errors = new LinkedHashSet(); @@ -62,14 +61,15 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { - errors.add(buildValidationMessage(pname, at, - executionContext.getExecutionConfig().getLocale(), propertyDeps.toString())); + errors.add(message().property(pname).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(propertyDeps.toString()).build()); } } } JsonSchema schema = schemaDeps.get(pname); if (schema != null) { - errors.addAll(schema.validate(executionContext, node, rootNode, at)); + errors.addAll(schema.validate(executionContext, node, rootNode, instanceLocation)); } } diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index bf3f50042..24248be27 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -26,9 +26,9 @@ public class DependentRequired extends BaseJsonValidator implements JsonValidato private static final Logger logger = LoggerFactory.getLogger(DependentRequired.class); private final Map> propertyDependencies = new HashMap>(); - public DependentRequired(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public DependentRequired(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENT_REQUIRED, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENT_REQUIRED, validationContext); for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); @@ -41,12 +41,10 @@ public DependentRequired(String schemaPath, JsonNode schemaNode, JsonSchema pare } } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); Set errors = new LinkedHashSet(); @@ -56,8 +54,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (dependencies != null && !dependencies.isEmpty()) { for (String field : dependencies) { if (node.get(field) == null) { - errors.add(buildValidationMessage(pname, at, executionContext.getExecutionConfig().getLocale(), - field, pname)); + errors.add(message().property(pname).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(field, pname) + .build()); } } } diff --git a/src/main/java/com/networknt/schema/DependentSchemas.java b/src/main/java/com/networknt/schema/DependentSchemas.java index ea341aa19..51ee49eac 100644 --- a/src/main/java/com/networknt/schema/DependentSchemas.java +++ b/src/main/java/com/networknt/schema/DependentSchemas.java @@ -26,24 +26,23 @@ public class DependentSchemas extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DependentSchemas.class); private final Map schemaDependencies = new HashMap<>(); - public DependentSchemas(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public DependentSchemas(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENT_SCHEMAS, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENT_SCHEMAS, validationContext); for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); JsonNode pvalue = schemaNode.get(pname); if (pvalue.isObject() || pvalue.isBoolean()) { - this.schemaDependencies.put(pname, validationContext.newSchema(pname, pvalue, parentSchema)); + this.schemaDependencies.put(pname, validationContext.newSchema(schemaLocation.append(pname), + evaluationPath.append(pname), pvalue, parentSchema)); } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); Set errors = new LinkedHashSet<>(); @@ -51,7 +50,7 @@ public Set validate(ExecutionContext executionContext, JsonNo String pname = it.next(); JsonSchema schema = this.schemaDependencies.get(pname); if (schema != null) { - errors.addAll(schema.validate(executionContext, node, rootNode, at)); + errors.addAll(schema.validate(executionContext, node, rootNode, instanceLocation)); } } @@ -64,12 +63,12 @@ public void preloadJsonSchema() { } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } for (JsonSchema schema : this.schemaDependencies.values()) { - schema.walk(executionContext, node, rootNode, at, false); + schema.walk(executionContext, node, rootNode, instanceLocation, false); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 1f10ef451..459b63081 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -32,8 +32,8 @@ public class EnumValidator extends BaseJsonValidator implements JsonValidator { private final Set nodes; private final String error; - public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ENUM, validationContext); + public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ENUM, validationContext); this.validationContext = validationContext; if (schemaNode != null && schemaNode.isArray()) { nodes = new HashSet(); @@ -73,16 +73,15 @@ public EnumValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSc nodes = Collections.emptySet(); error = "[none]"; } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue()); if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), error)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ErrorMessageType.java b/src/main/java/com/networknt/schema/ErrorMessageType.java index b7751cfd4..72b0acbd0 100644 --- a/src/main/java/com/networknt/schema/ErrorMessageType.java +++ b/src/main/java/com/networknt/schema/ErrorMessageType.java @@ -16,8 +16,6 @@ package com.networknt.schema; -import java.util.Map; - public interface ErrorMessageType { /** * Your error code. Please ensure global uniqueness. Builtin error codes are sequential numbers. @@ -28,10 +26,6 @@ public interface ErrorMessageType { */ String getErrorCode(); - default Map getCustomMessage() { - return null; - } - /** * Get the text representation of the error code. * diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index 73c285f66..12779bd37 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -32,15 +32,12 @@ public class ExclusiveMaximumValidator extends BaseJsonValidator { private final ThresholdMixin typedMaximum; - public ExclusiveMaximumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MAXIMUM, validationContext); + public ExclusiveMaximumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MAXIMUM, validationContext); this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("exclusiveMaximum value is not a number"); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); - final String maximumText = schemaNode.asText(); if ((schemaNode.isLong() || schemaNode.isInt()) && (JsonType.INTEGER.toString().equals(getNodeFieldType()))) { // "integer", and within long range @@ -98,8 +95,8 @@ public String thresholdValue() { } } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!JsonNodeUtil.isNumber(node, validationContext.getConfig())) { // maximum only applies to numbers @@ -107,8 +104,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index 62a20af4a..879bbbb41 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -36,15 +36,12 @@ public class ExclusiveMinimumValidator extends BaseJsonValidator { */ private final ThresholdMixin typedMinimum; - public ExclusiveMinimumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MINIMUM, validationContext); + public ExclusiveMinimumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.EXCLUSIVE_MINIMUM, validationContext); this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("exclusiveMinimum value is not a number"); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); - final String minimumText = schemaNode.asText(); if ((schemaNode.isLong() || schemaNode.isInt()) && JsonType.INTEGER.toString().equals(getNodeFieldType())) { // "integer", and within long range @@ -105,8 +102,8 @@ public String thresholdValue() { } } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!JsonNodeUtil.isNumber(node, this.validationContext.getConfig())) { // minimum only applies to numbers @@ -114,8 +111,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index b3f1ca4e0..81ac03d0e 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -18,12 +18,14 @@ import java.util.Locale; import java.util.Objects; +import java.util.function.Predicate; /** * Configuration per execution. */ public class ExecutionConfig { private Locale locale = Locale.ROOT; + private Predicate annotationAllowedPredicate = (keyword) -> true; public Locale getLocale() { return locale; @@ -33,4 +35,53 @@ public void setLocale(Locale locale) { this.locale = Objects.requireNonNull(locale, "Locale must not be null"); } + /** + * Gets the predicate to determine if annotation collection is allowed for a + * particular keyword. + *

+ * The default value is to allow annotation collection. + *

+ * Setting this to return false improves performance but keywords such as + * unevaluatedItems and unevaluatedProperties will fail to evaluate properly. + *

+ * This will also affect reporting if annotations need to be in the output + * format. + *

+ * unevaluatedProperties depends on properties, patternProperties and + * additionalProperties. + *

+ * unevaluatedItems depends on items/prefixItems, additionalItems/items and + * contains. + * + * @return the predicate to determine if annotation collection is allowed for + * the keyword + */ + public Predicate getAnnotationAllowedPredicate() { + return annotationAllowedPredicate; + } + + /** + * Predicate to determine if annotation collection is allowed for a particular + * keyword. + *

+ * The default value is to allow annotation collection. + *

+ * Setting this to return false improves performance but keywords such as + * unevaluatedItems and unevaluatedProperties will fail to evaluate properly. + *

+ * This will also affect reporting if annotations need to be in the output + * format. + *

+ * unevaluatedProperties depends on properties, patternProperties and + * additionalProperties. + *

+ * unevaluatedItems depends on items/prefixItems, additionalItems/items and + * contains. + * + * @param annotationAllowedPredicate the predicate accepting the keyword + */ + public void setAnnotationAllowedPredicate(Predicate annotationAllowedPredicate) { + this.annotationAllowedPredicate = Objects.requireNonNull(annotationAllowedPredicate, + "annotationAllowedPredicate must not be null"); + } } diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index 96d33fd0e..0e14dd991 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -22,37 +22,95 @@ public class ExecutionContext { private ExecutionConfig executionConfig; private CollectorContext collectorContext; + private ValidatorState validatorState = null; + /** + * Creates an execution context. + */ public ExecutionContext() { this(new CollectorContext()); } + /** + * Creates an execution context. + * + * @param collectorContext the collector context + */ public ExecutionContext(CollectorContext collectorContext) { this(new ExecutionConfig(), collectorContext); } - + + /** + * Creates an execution context. + * + * @param executionConfig the execution configuration + */ public ExecutionContext(ExecutionConfig executionConfig) { this(executionConfig, new CollectorContext()); } - + + /** + * Creates an execution context. + * + * @param executionConfig the execution configuration + * @param collectorContext the collector context + */ public ExecutionContext(ExecutionConfig executionConfig, CollectorContext collectorContext) { this.collectorContext = collectorContext; this.executionConfig = executionConfig; } + /** + * Gets the collector context. + * + * @return the collector context + */ public CollectorContext getCollectorContext() { return collectorContext; } + /** + * Sets the collector context. + * + * @param collectorContext the collector context + */ public void setCollectorContext(CollectorContext collectorContext) { this.collectorContext = collectorContext; } + /** + * Gets the execution configuration. + * + * @return the execution configuration + */ public ExecutionConfig getExecutionConfig() { return executionConfig; } + /** + * Sets the execution configuration. + * + * @param executionConfig the execution configuration + */ public void setExecutionConfig(ExecutionConfig executionConfig) { this.executionConfig = executionConfig; } + + /** + * Gets the validator state. + * + * @return the validator state + */ + public ValidatorState getValidatorState() { + return validatorState; + } + + /** + * Sets the validator state. + * + * @param validatorState the validator state + */ + public void setValidatorState(ValidatorState validatorState) { + this.validatorState = validatorState; + } } diff --git a/src/main/java/com/networknt/schema/ExecutionCustomizer.java b/src/main/java/com/networknt/schema/ExecutionCustomizer.java new file mode 100644 index 000000000..7ae5d1a56 --- /dev/null +++ b/src/main/java/com/networknt/schema/ExecutionCustomizer.java @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Customize the execution context before validation. + */ +@FunctionalInterface +interface ExecutionCustomizer { + /** + * Customize the execution context before validation. + *

+ * The validation context should only be used for reference as it is shared. + * + * @param executionContext the execution context + * @param validationContext the validation context for reference + */ + void customize(ExecutionContext executionContext, ValidationContext validationContext); +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 10c4a9133..675b4a601 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -25,13 +25,14 @@ public class FalseValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(FalseValidator.class); - public FalseValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.FALSE, validationContext); + public FalseValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.FALSE, validationContext); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); // For the false validator, it is always not valid - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).build()); } } diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java index 5074e7d37..214a4f00a 100644 --- a/src/main/java/com/networknt/schema/FormatKeyword.java +++ b/src/main/java/com/networknt/schema/FormatKeyword.java @@ -41,13 +41,13 @@ Collection getFormats() { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { Format format = null; if (schemaNode != null && schemaNode.isTextual()) { String formatName = schemaNode.textValue(); format = this.formats.get(formatName); if (format != null) { - return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format, type); + return new FormatValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format, type); } switch (formatName) { @@ -57,23 +57,16 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc case DATE_TIME: { ValidatorTypeCode typeCode = ValidatorTypeCode.DATETIME; - // Set custom error message - typeCode.setCustomMessage(this.type.getCustomMessage()); - return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext, typeCode); + return new DateTimeValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, typeCode); } } } - return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format, this.type); + return new FormatValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format, this.type); } @Override public String getValue() { return this.type.getValue(); } - - @Override - public void setCustomMessage(Map message) { - this.type.setCustomMessage(message); - } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index 49a739156..14d005780 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -30,15 +30,14 @@ public class FormatValidator extends BaseJsonValidator implements JsonValidator private final Format format; - public FormatValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Format format, ValidatorTypeCode type) { - super(schemaPath, schemaNode, parentSchema, type, validationContext); + public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Format format, ValidatorTypeCode type) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, type, validationContext); this.format = format; this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { @@ -50,22 +49,25 @@ public Set validate(ExecutionContext executionContext, JsonNo if(format.getName().equals("ipv6")) { if(!node.textValue().trim().equals(node.textValue())) { // leading and trailing spaces - errors.add(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(format.getName(), format.getErrorMessageDescription()).build()); } else if(node.textValue().contains("%")) { // zone id is not part of the ipv6 - errors.add(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(format.getName(), format.getErrorMessageDescription()).build()); } } try { if (!format.matches(executionContext, node.textValue())) { - errors.add(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), format.getName(), format.getErrorMessageDescription())); + errors.add(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(format.getName(), format.getErrorMessageDescription()).build()); } } catch (PatternSyntaxException pse) { // String is considered valid if pattern is invalid - logger.error("Failed to apply pattern on {}: Invalid RE syntax [{}]", at, format.getName(), pse); + logger.error("Failed to apply pattern on {}: Invalid RE syntax [{}]", instanceLocation, format.getName(), pse); } } diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index a3a511aa8..0d90912ff 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -27,28 +27,32 @@ public class IfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(IfValidator.class); - private static final ArrayList KEYWORDS = new ArrayList<>(Arrays.asList("if", "then", "else")); + private static final List KEYWORDS = Arrays.asList("if", "then", "else"); private final JsonSchema ifSchema; private final JsonSchema thenSchema; private final JsonSchema elseSchema; - public IfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.IF_THEN_ELSE, validationContext); + public IfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.IF_THEN_ELSE, validationContext); JsonSchema foundIfSchema = null; JsonSchema foundThenSchema = null; JsonSchema foundElseSchema = null; for (final String keyword : KEYWORDS) { - final JsonNode node = schemaNode.get(keyword); - final String schemaPathOfSchema = parentSchema.schemaPath + "/" + keyword; + final JsonNode node = parentSchema.getSchemaNode().get(keyword); + final SchemaLocation schemaLocationOfSchema = parentSchema.schemaLocation.append(keyword); + final JsonNodePath evaluationPathOfSchema = parentSchema.evaluationPath.append(keyword); if (keyword.equals("if")) { - foundIfSchema = validationContext.newSchema(schemaPathOfSchema, node, parentSchema); + foundIfSchema = validationContext.newSchema(schemaLocationOfSchema, evaluationPathOfSchema, node, + parentSchema); } else if (keyword.equals("then") && node != null) { - foundThenSchema = validationContext.newSchema(schemaPathOfSchema, node, parentSchema); + foundThenSchema = validationContext.newSchema(schemaLocationOfSchema, evaluationPathOfSchema, node, + parentSchema); } else if (keyword.equals("else") && node != null) { - foundElseSchema = validationContext.newSchema(schemaPathOfSchema, node, parentSchema); + foundElseSchema = validationContext.newSchema(schemaLocationOfSchema, evaluationPathOfSchema, node, + parentSchema); } } @@ -58,8 +62,8 @@ public IfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSche } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new LinkedHashSet<>(); @@ -68,7 +72,7 @@ public Set validate(ExecutionContext executionContext, JsonNo boolean ifConditionPassed = false; try { try { - ifConditionPassed = this.ifSchema.validate(executionContext, node, rootNode, at).isEmpty(); + ifConditionPassed = this.ifSchema.validate(executionContext, node, rootNode, instanceLocation).isEmpty(); } catch (JsonSchemaException ex) { // When failFast is enabled, validations are thrown as exceptions. // An exception means the condition failed @@ -76,13 +80,13 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (ifConditionPassed && this.thenSchema != null) { - errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, at)); + errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, instanceLocation)); } else if (!ifConditionPassed && this.elseSchema != null) { // discard ifCondition results collectorContext.exitDynamicScope(); collectorContext.enterDynamicScope(); - errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, at)); + errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, instanceLocation)); } } finally { @@ -109,19 +113,19 @@ public void preloadJsonSchema() { } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } if (null != this.ifSchema) { - this.ifSchema.walk(executionContext, node, rootNode, at, false); + this.ifSchema.walk(executionContext, node, rootNode, instanceLocation, false); } if (null != this.thenSchema) { - this.thenSchema.walk(executionContext, node, rootNode, at, false); + this.thenSchema.walk(executionContext, node, rootNode, instanceLocation, false); } if (null != this.elseSchema) { - this.elseSchema.walk(executionContext, node, rootNode, at, false); + this.elseSchema.walk(executionContext, node, rootNode, instanceLocation, false); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 9e151b4c3..98cce6a85 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -36,18 +36,21 @@ public class ItemsValidator extends BaseJsonValidator { private final JsonSchema additionalSchema; private WalkListenerRunner arrayItemWalkListenerRunner; - public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext); + public ItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext); this.tupleSchema = new ArrayList<>(); JsonSchema foundSchema = null; JsonSchema foundAdditionalSchema = null; if (schemaNode.isObject() || schemaNode.isBoolean()) { - foundSchema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); + foundSchema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { + int i = 0; for (JsonNode s : schemaNode) { - this.tupleSchema.add(validationContext.newSchema(schemaPath, s, parentSchema)); + this.tupleSchema.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), + s, parentSchema)); + i++; } JsonNode addItemNode = getParentSchema().getSchemaNode().get(PROPERTY_ADDITIONAL_ITEMS); @@ -55,7 +58,9 @@ public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS if (addItemNode.isBoolean()) { this.additionalItems = addItemNode.asBoolean(); } else if (addItemNode.isObject()) { - foundAdditionalSchema = validationContext.newSchema("#", addItemNode, parentSchema); + foundAdditionalSchema = validationContext.newSchema( + parentSchema.schemaLocation.append(PROPERTY_ADDITIONAL_ITEMS), + parentSchema.evaluationPath.append(PROPERTY_ADDITIONAL_ITEMS), addItemNode, parentSchema); } } } @@ -63,44 +68,44 @@ public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentS this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); - this.schema = foundSchema; this.additionalSchema = foundAdditionalSchema; } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); - - Set errors = new LinkedHashSet<>(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!node.isArray() && !this.validationContext.getConfig().isTypeLoose()) { // ignores non-arrays - return errors; + return Collections.emptySet(); } + Set errors = new LinkedHashSet<>(); if (node.isArray()) { int i = 0; for (JsonNode n : node) { - doValidate(executionContext, errors, i, n, rootNode, at); + doValidate(executionContext, errors, i, n, rootNode, instanceLocation); i++; } } else { - doValidate(executionContext, errors, 0, node, rootNode, at); + doValidate(executionContext, errors, 0, node, rootNode, instanceLocation); } - return Collections.unmodifiableSet(errors); + return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } - private void doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, JsonNode rootNode, String at) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); - String path = atPath(at, i); + private void doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation) { + Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + JsonNodePath path = instanceLocation.append(i); if (this.schema != null) { // validate with item schema (the whole array has the same item // schema) Set results = this.schema.validate(executionContext, node, rootNode, path); if (results.isEmpty()) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { errors.addAll(results); } @@ -109,7 +114,9 @@ private void doValidate(ExecutionContext executionContext, Set results = this.tupleSchema.get(i).validate(executionContext, node, rootNode, path); if (results.isEmpty()) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { errors.addAll(results); } @@ -118,17 +125,21 @@ private void doValidate(ExecutionContext executionContext, Set results = this.additionalSchema.validate(executionContext, node, rootNode, path); if (results.isEmpty()) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { errors.addAll(results); } } else if (this.additionalItems != null) { if (this.additionalItems) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { // no additional item allowed, return error - errors.add( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + i)); + errors.add(message().instanceLocation(path) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(i).build()); } } } @@ -138,7 +149,7 @@ private void doValidate(ExecutionContext executionContext, Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { HashSet validationMessages = new LinkedHashSet<>(); if (node instanceof ArrayNode) { ArrayNode arrayNode = (ArrayNode) node; @@ -152,31 +163,31 @@ public Set walk(ExecutionContext executionContext, JsonNode n arrayNode.set(i, defaultNode); n = defaultNode; } - doWalk(executionContext, validationMessages, i, n, rootNode, at, shouldValidateSchema); + doWalk(executionContext, validationMessages, i, n, rootNode, instanceLocation, shouldValidateSchema); i++; } } else { - doWalk(executionContext, validationMessages, 0, node, rootNode, at, shouldValidateSchema); + doWalk(executionContext, validationMessages, 0, node, rootNode, instanceLocation, shouldValidateSchema); } return validationMessages; } private void doWalk(ExecutionContext executionContext, HashSet validationMessages, int i, JsonNode node, - JsonNode rootNode, String at, boolean shouldValidateSchema) { + JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (this.schema != null) { // Walk the schema. - walkSchema(executionContext, this.schema, node, rootNode, atPath(at, i), shouldValidateSchema, validationMessages); + walkSchema(executionContext, this.schema, node, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages); } if (this.tupleSchema != null) { if (i < this.tupleSchema.size()) { // walk tuple schema - walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, atPath(at, i), + walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages); } else { if (this.additionalSchema != null) { // walk additional item schema - walkSchema(executionContext, this.additionalSchema, node, rootNode, atPath(at, i), + walkSchema(executionContext, this.additionalSchema, node, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages); } } @@ -184,16 +195,16 @@ private void doWalk(ExecutionContext executionContext, HashSet validationMessages) { + JsonNodePath instanceLocation, boolean shouldValidateSchema, Set validationMessages) { boolean executeWalk = this.arrayItemWalkListenerRunner.runPreWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(), - node, rootNode, at, walkSchema.getSchemaPath(), walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory()); + node, rootNode, instanceLocation, walkSchema.getEvaluationPath(), walkSchema.getSchemaLocation(), + walkSchema.getSchemaNode(), walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory()); if (executeWalk) { - validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, at, shouldValidateSchema)); + validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); } this.arrayItemWalkListenerRunner.runPostWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(), node, rootNode, - at, walkSchema.getSchemaPath(), walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); + instanceLocation, this.evaluationPath, walkSchema.getSchemaLocation(), + walkSchema.getSchemaNode(), walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index a1e63e542..7deb96937 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -33,8 +33,8 @@ public class ItemsValidator202012 extends BaseJsonValidator { private final WalkListenerRunner arrayItemWalkListenerRunner; private final int prefixCount; - public ItemsValidator202012(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS_202012, validationContext); + public ItemsValidator202012(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS_202012, validationContext); JsonNode prefixItems = parentSchema.getSchemaNode().get("prefixItems"); if (prefixItems instanceof ArrayNode) { @@ -46,7 +46,7 @@ public ItemsValidator202012(String schemaPath, JsonNode schemaNode, JsonSchema p } if (schemaNode.isObject() || schemaNode.isBoolean()) { - this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); + this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { throw new IllegalArgumentException("The value of 'items' MUST be a valid JSON Schema."); } @@ -54,36 +54,36 @@ public ItemsValidator202012(String schemaPath, JsonNode schemaNode, JsonSchema p this.arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners()); this.validationContext = validationContext; - - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); - - Set errors = new LinkedHashSet<>(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); // ignores non-arrays if (node.isArray()) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + Set errors = new LinkedHashSet<>(); + Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); for (int i = this.prefixCount; i < node.size(); ++i) { - String path = atPath(at, i); + JsonNodePath path = instanceLocation.append(i); // validate with item schema (the whole array has the same item schema) Set results = this.schema.validate(executionContext, node.get(i), rootNode, path); if (results.isEmpty()) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { errors.addAll(results); } } + return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + } else { + return Collections.emptySet(); } - - return Collections.unmodifiableSet(errors); } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { Set validationMessages = new LinkedHashSet<>(); if (node instanceof ArrayNode) { @@ -99,42 +99,43 @@ public Set walk(ExecutionContext executionContext, JsonNode n n = defaultNode; } // Walk the schema. - walkSchema(executionContext, this.schema, n, rootNode, atPath(at, i), shouldValidateSchema, validationMessages); + walkSchema(executionContext, this.schema, n, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages); } } else { - walkSchema(executionContext, this.schema, node, rootNode, at, shouldValidateSchema, validationMessages); + walkSchema(executionContext, this.schema, node, rootNode, instanceLocation, shouldValidateSchema, validationMessages); } return validationMessages; } - private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema, Set validationMessages) { + private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema, Set validationMessages) { //@formatter:off boolean executeWalk = this.arrayItemWalkListenerRunner.runPreWalkListeners( executionContext, ValidatorTypeCode.ITEMS.getValue(), node, rootNode, - at, - walkSchema.getSchemaPath(), + instanceLocation, + walkSchema.getEvaluationPath(), + walkSchema.getSchemaLocation(), walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), - this.validationContext, this.validationContext.getJsonSchemaFactory() + walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory() ); if (executeWalk) { - validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, at, shouldValidateSchema)); + validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); } this.arrayItemWalkListenerRunner.runPostWalkListeners( executionContext, ValidatorTypeCode.ITEMS.getValue(), node, rootNode, - at, - walkSchema.getSchemaPath(), + instanceLocation, + this.evaluationPath, + walkSchema.getSchemaLocation(), walkSchema.getSchemaNode(), walkSchema.getParentSchema(), - this.validationContext, - this.validationContext.getJsonSchemaFactory(), validationMessages + this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages ); //@formatter:on } diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 7efb4b81f..71a80d037 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -263,8 +263,8 @@ public Map getKeywords() { return this.keywords; } - public JsonValidator newValidator(ValidationContext validationContext, String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, Map customMessage) { + public JsonValidator newValidator(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword /* keyword */, JsonNode schemaNode, + JsonSchema parentSchema) { try { Keyword kw = this.keywords.get(keyword); @@ -274,8 +274,7 @@ public JsonValidator newValidator(ValidationContext validationContext, String sc } return null; } - kw.setCustomMessage(customMessage); - return kw.newValidator(schemaPath, schemaNode, parentSchema, validationContext); + return kw.newValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext); } catch (InvocationTargetException e) { if (e.getTargetException() instanceof JsonSchemaException) { logger.error("Error:", e); diff --git a/src/main/java/com/networknt/schema/JsonNodePath.java b/src/main/java/com/networknt/schema/JsonNodePath.java new file mode 100644 index 000000000..d7210be90 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonNodePath.java @@ -0,0 +1,235 @@ +/* + * 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.Objects; + +/** + * Represents a path to a JSON node. + */ +public class JsonNodePath implements Comparable { + private final PathType type; + private final JsonNodePath parent; + + private final String pathSegment; + private final int pathSegmentIndex; + + private volatile String value = null; // computed lazily + + public JsonNodePath(PathType type) { + this.type = type; + this.parent = null; + this.pathSegment = null; + this.pathSegmentIndex = -1; + } + + private JsonNodePath(JsonNodePath parent, String pathSegment) { + this.parent = parent; + this.type = parent.type; + this.pathSegment = pathSegment; + this.pathSegmentIndex = -1; + } + + private JsonNodePath(JsonNodePath parent, int pathSegmentIndex) { + this.parent = parent; + this.type = parent.type; + this.pathSegment = null; + this.pathSegmentIndex = pathSegmentIndex; + } + + /** + * Returns the parent path, or null if this path does not have a parent. + * + * @return the parent + */ + public JsonNodePath getParent() { + return this.parent; + } + + /** + * Append the child token to the path. + * + * @param token the child token + * @return the path + */ + public JsonNodePath append(String token) { + return new JsonNodePath(this, token); + } + + /** + * Append the index to the path. + * + * @param index the index + * @return the path + */ + public JsonNodePath append(int index) { + return new JsonNodePath(this, index); + } + + /** + * Gets the {@link PathType}. + * + * @return the path type + */ + public PathType getPathType() { + return this.type; + } + + /** + * Gets the name element given an index. + *

+ * The index parameter is the index of the name element to return. The element + * that is closest to the root has index 0. The element that is farthest from + * the root has index count -1. + * + * @param index to return + * @return the name element + */ + public String getName(int index) { + Object element = getElement(index); + if (element != null) { + return element.toString(); + } + return null; + } + + /** + * Gets the element given an index. + *

+ * The index parameter is the index of the element to return. The element that + * is closest to the root has index 0. The element that is farthest from the + * root has index count -1. + * + * @param index to return + * @return the element either a String or Integer + */ + public Object getElement(int index) { + if (index == -1) { + if (this.pathSegmentIndex != -1) { + return Integer.valueOf(this.pathSegmentIndex); + } else { + return this.pathSegment; + } + } + int nameCount = getNameCount(); + if (nameCount - 1 == index) { + return this.getElement(-1); + } + int count = nameCount - index - 1; + if (count < 0) { + throw new IllegalArgumentException(""); + } + JsonNodePath current = this; + for (int x = 0; x < count; x++) { + current = current.parent; + } + return current.getElement(-1); + } + + /** + * Gets the number of name elements in the path. + * + * @return the number of elements in the path or 0 if this is the root element + */ + public int getNameCount() { + int current = this.pathSegmentIndex == -1 && this.pathSegment == null ? 0 : 1; + int parent = this.parent != null ? this.parent.getNameCount() : 0; + return current + parent; + } + + /** + * Tests if this path starts with the other path. + * + * @param other the other path + * @return true if the path starts with the other path + */ + public boolean startsWith(JsonNodePath other) { + int count = getNameCount(); + int otherCount = other.getNameCount(); + + if (otherCount > count) { + return false; + } else if (otherCount == count) { + return this.equals(other); + } else { + JsonNodePath compare = this; + int x = count - otherCount; + while (x > 0) { + compare = compare.getParent(); + x--; + } + return other.equals(compare); + } + } + + @Override + public String toString() { + if (this.value == null) { + String parentValue = this.parent == null ? type.getRoot() : this.parent.toString(); + if (pathSegmentIndex != -1) { + this.value = this.type.append(parentValue, pathSegmentIndex); + } else if (pathSegment != null) { + this.value = this.type.append(parentValue, pathSegment); + } else { + this.value = parentValue; + } + } + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(parent, pathSegment, pathSegmentIndex, type); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonNodePath other = (JsonNodePath) obj; + return Objects.equals(parent, other.parent) && Objects.equals(pathSegment, other.pathSegment) + && pathSegmentIndex == other.pathSegmentIndex && type == other.type; + } + + @Override + public int compareTo(JsonNodePath other) { + if (this.parent != null && other.parent == null) { + return 1; + } else if (this.parent == null && other.parent != null) { + return -1; + } else if (this.parent != null && other.parent != null) { + int result = this.parent.compareTo(other.parent); + if (result != 0) { + return result; + } + } + String thisValue = this.getName(-1); + String otherValue = other.getName(-1); + if (thisValue == null && otherValue == null) { + return 0; + } else if (thisValue != null && otherValue == null) { + return 1; + } else if (thisValue == null && otherValue != null) { + return -1; + } else { + return thisValue.compareTo(otherValue); + } + } +} diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 7d9cd8ecd..b142ac9aa 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -18,13 +18,11 @@ 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; import com.networknt.schema.utils.StringUtils; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; -import com.networknt.schema.walk.JsonSchemaWalker; import com.networknt.schema.walk.WalkListenerRunner; import java.io.UnsupportedEncodingException; @@ -41,7 +39,10 @@ public class JsonSchema extends BaseJsonValidator { private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue(); - private Map validators; + /** + * The validators sorted and indexed by evaluation path. + */ + private List validators; private final JsonMetaSchema metaSchema; private boolean validatorsLoaded = false; private boolean dynamicAnchor = false; @@ -62,54 +63,18 @@ public class JsonSchema extends BaseJsonValidator { WalkListenerRunner keywordWalkListenerRunner = null; - static JsonSchema from(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { - return new JsonSchema(validationContext, schemaPath, currentUri, schemaNode, parent, suppressSubSchemaRetrieval); + static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { + return new JsonSchema(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parent, suppressSubSchemaRetrieval); } - /** - * @param validationContext validation context - * @param baseUri base URL - * @param schemaNode schema node - * @deprecated Use {@code JsonSchemaFactory#create(ValidationContext, String, JsonNode, JsonSchema)} - */ - @Deprecated - public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode schemaNode) { - this(validationContext, "#", baseUri, schemaNode, null); - } - - /** - * @param validationContext validation context - * @param schemaPath schema path - * @param currentUri current URI - * @param schemaNode schema node - * @param parent parent schema - * @deprecated Use {@code JsonSchemaFactory#create(ValidationContext, String, JsonNode, JsonSchema)} - */ - @Deprecated - public JsonSchema(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode, - JsonSchema parent) { - this(validationContext, schemaPath, currentUri, schemaNode, parent, false); - } - - /** - * @param validationContext validation context - * @param baseUri base URI - * @param schemaNode schema node - * @param suppressSubSchemaRetrieval suppress sub schema retrieval - * @deprecated Use {@code JsonSchemaFactory#create(ValidationContext, String, JsonNode, JsonSchema)} - */ - @Deprecated - public JsonSchema(ValidationContext validationContext, URI baseUri, JsonNode schemaNode, boolean suppressSubSchemaRetrieval) { - this(validationContext, "#", baseUri, schemaNode, null, suppressSubSchemaRetrieval); - } - - private JsonSchema(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode, - JsonSchema parent, boolean suppressSubSchemaRetrieval) { - super(schemaPath, schemaNode, parent, null, validationContext, suppressSubSchemaRetrieval); + private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, + JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { + super(schemaLocation, evaluationPath, schemaNode, parent, null, null, validationContext, + suppressSubSchemaRetrieval); this.validationContext = validationContext; this.metaSchema = validationContext.getMetaSchema(); this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode); - if (uriRefersToSubschema(currentUri, schemaPath)) { + if (uriRefersToSubschema(currentUri, schemaLocation)) { updateThisAsSubschema(currentUri); } if (validationContext.getConfig() != null) { @@ -117,14 +82,14 @@ private JsonSchema(ValidationContext validationContext, String schemaPath, URI c if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator"); if (null != discriminator && null != validationContext.getCurrentDiscriminatorContext()) { - validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaPath, discriminator); + validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaLocation, discriminator); } } } } - public JsonSchema createChildSchema(String schemaPath, JsonNode schemaNode) { - return getValidationContext().newSchema(schemaPath, schemaNode, this); + public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { + return getValidationContext().newSchema(schemaLocation, evaluationPath, schemaNode, this); } ValidationContext getValidationContext() { @@ -141,9 +106,11 @@ private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { try { return this.validationContext.getURIFactory().create(currentUri, id); } catch (IllegalArgumentException e) { + SchemaLocation path = schemaLocation.append(this.metaSchema.getIdKeyword()); ValidationMessage validationMessage = ValidationMessage.builder().code(ValidatorTypeCode.ID.getValue()) - .type(ValidatorTypeCode.ID.getValue()).path(id).schemaPath(schemaPath) - .arguments(currentUri == null ? "null" : currentUri.toString()) + .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()) + .evaluationPath(path.getFragment()) + .arguments(currentUri == null ? "null" : currentUri.toString(), id) .messageFormatter(args -> this.validationContext.getConfig().getMessageSource().getMessage( ValidatorTypeCode.ID.getValue(), this.validationContext.getConfig().getLocale(), args)) .build(); @@ -156,10 +123,10 @@ private static boolean isUriFragmentWithNoContext(URI currentUri, String id) { return id.startsWith("#") && currentUri == null; } - private static boolean uriRefersToSubschema(URI originalUri, String schemaPath) { + private static boolean uriRefersToSubschema(URI originalUri, SchemaLocation schemaLocation) { return originalUri != null && StringUtils.isNotBlank(originalUri.getRawFragment()) // Original currentUri parameter has a fragment, so it refers to a subschema - && (StringUtils.isBlank(schemaPath) || "#".equals(schemaPath)); // We aren't already in a subschema + && (schemaLocation.getFragment().getNameCount() == 0); // We aren't already in a subschema } /** @@ -179,8 +146,8 @@ private void updateThisAsSubschema(URI originalUri) { } catch (URISyntaxException ex) { throw new JsonSchemaException("Unable to create URI without fragment from " + this.currentUri + ": " + ex.getMessage()); } - this.parentSchema = new JsonSchema(this.validationContext, this.schemaPath, currentUriWithoutFragment, this.schemaNode, this.parentSchema, super.suppressSubSchemaRetrieval); // TODO: Should this be delegated to the factory? - this.schemaPath = fragment; + this.parentSchema = new JsonSchema(this.validationContext, SchemaLocation.of(currentUriWithoutFragment.toString()), this.evaluationPath, currentUriWithoutFragment, this.schemaNode, this.parentSchema, super.suppressSubSchemaRetrieval); // TODO: Should this be delegated to the factory? + this.schemaLocation = SchemaLocation.of(originalUri.toString()); this.schemaNode = fragmentSchemaNode; this.currentUri = combineCurrentUriWithIds(this.currentUri, fragmentSchemaNode); } @@ -252,20 +219,21 @@ private JsonNode handleNullNode(String ref, JsonSchema schema) { } /** - * Please note that the key in {@link #validators} map is a schema path. It is - * used in {@link com.networknt.schema.walk.DefaultKeywordWalkListenerRunner} to derive the keyword. + * Please note that the key in {@link #validators} map is the evaluation path. */ - private Map read(JsonNode schemaNode) { - Map validators = new TreeMap<>(VALIDATOR_SORT); + private List read(JsonNode schemaNode) { + List validators = new ArrayList<>(); if (schemaNode.isBoolean()) { if (schemaNode.booleanValue()) { - final Map customMessage = getCustomMessage(schemaNode, "true"); - JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "true", schemaNode, this, customMessage); - validators.put(getSchemaPath() + "/true", validator); + JsonNodePath path = getEvaluationPath().append("true"); + JsonValidator validator = this.validationContext.newValidator(getSchemaLocation().append("true"), path, + "true", schemaNode, this); + validators.add(validator); } else { - final Map customMessage = getCustomMessage(schemaNode, "false"); - JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), "false", schemaNode, this, customMessage); - validators.put(getSchemaPath() + "/false", validator); + JsonNodePath path = getEvaluationPath().append("false"); + JsonValidator validator = this.validationContext.newValidator(getSchemaLocation().append("false"), + path, "false", schemaNode, this); + validators.add(validator); } } else { @@ -276,8 +244,10 @@ private Map read(JsonNode schemaNode) { Iterator pnames = schemaNode.fieldNames(); while (pnames.hasNext()) { String pname = pnames.next(); - JsonNode nodeToUse = pname.equals("if") ? schemaNode : schemaNode.get(pname); - Map customMessage = getCustomMessage(schemaNode, pname); + JsonNode nodeToUse = schemaNode.get(pname); + + JsonNodePath path = getEvaluationPath().append(pname); + SchemaLocation schemaPath = getSchemaLocation().append(pname); if ("$recursiveAnchor".equals(pname)) { if (!nodeToUse.isBoolean()) { @@ -285,8 +255,9 @@ private Map read(JsonNode schemaNode) { .code("internal.invalidRecursiveAnchor") .message( "{0}: The value of a $recursiveAnchor must be a Boolean literal but is {1}") - .path(schemaPath) - .schemaPath(schemaPath) + .instanceLocation(path) + .evaluationPath(path) + .schemaLocation(schemaPath) .arguments(nodeToUse.getNodeType().toString()) .build(); throw new JsonSchemaException(validationMessage); @@ -294,19 +265,16 @@ private Map read(JsonNode schemaNode) { this.dynamicAnchor = nodeToUse.booleanValue(); } - JsonValidator validator = this.validationContext.newValidator(getSchemaPath(), pname, nodeToUse, this, customMessage); + JsonValidator validator = this.validationContext.newValidator(schemaPath, path, + pname, nodeToUse, this); if (validator != null) { - validators.put(getSchemaPath() + "/" + pname, validator); + validators.add(validator); if ("$ref".equals(pname)) { refValidator = validator; - } - - if ("required".equals(pname)) { + } else if ("required".equals(pname)) { this.requiredValidator = validator; - } - - if ("type".equals(pname)) { + } else if ("type".equals(pname)) { this.typeValidator = (TypeValidator) validator; } } @@ -316,10 +284,12 @@ private Map read(JsonNode schemaNode) { // Ignore siblings for older drafts if (null != refValidator && activeDialect() < V201909_VALUE) { validators.clear(); - validators.put(getSchemaPath() + "/$ref", refValidator); + validators.add(refValidator); } } - + if (validators.size() > 1) { + Collections.sort(validators, VALIDATOR_SORT); + } return validators; } @@ -334,82 +304,49 @@ private long activeDialect() { * A comparator that sorts validators, such that 'properties' comes before 'required', * so that we can apply default values before validating required. */ - private static Comparator VALIDATOR_SORT = (lhs, rhs) -> { - if (lhs.equals(rhs)) return 0; - if (lhs.endsWith("/properties")) return -1; - if (rhs.endsWith("/properties")) return 1; - if (lhs.endsWith("/patternProperties")) return -1; - if (rhs.endsWith("/patternProperties")) return 1; - if (lhs.endsWith("/unevaluatedItems")) return 1; - if (rhs.endsWith("/unevaluatedItems")) return -1; - if (lhs.endsWith("/unevaluatedProperties")) return 1; - if (rhs.endsWith("/unevaluatedProperties")) return -1; - - return lhs.compareTo(rhs); // TODO: This smells. We are performing a lexicographical ordering of paths of unknown depth. + private static Comparator VALIDATOR_SORT = (lhs, rhs) -> { + String lhsName = lhs.getEvaluationPath().getName(-1); + String rhsName = rhs.getEvaluationPath().getName(-1); + + if (lhsName.equals(rhsName)) return 0; + + if (lhsName.equals("properties")) return -1; + if (rhsName.equals("properties")) return 1; + if (lhsName.equals("patternProperties")) return -1; + if (rhsName.equals("patternProperties")) return 1; + if (lhsName.equals("unevaluatedItems")) return 1; + if (rhsName.equals("unevaluatedItems")) return -1; + if (lhsName.equals("unevaluatedProperties")) return 1; + if (rhsName.equals("unevaluatedProperties")) return -1; + + return 0; // retain original schema definition order }; - private Map getCustomMessage(JsonNode schemaNode, String pname) { - if (!this.validationContext.getConfig().isCustomMessageSupported()) { - return null; - } - final JsonSchema parentSchema = getParentSchema(); - final JsonNode message = getMessageNode(schemaNode, parentSchema, pname); - if (message != null) { - JsonNode messageNode = message.get(pname); - if (messageNode != null) { - if (messageNode.isTextual()) { - return Collections.singletonMap("", messageNode.asText()); - } else if (messageNode.isObject()) { - Map result = new LinkedHashMap<>(); - messageNode.fields().forEachRemaining(entry -> { - result.put(entry.getKey(), entry.getValue().textValue()); - }); - if (!result.isEmpty()) { - return result; - } - } - } - } - return Collections.emptyMap(); - } - - private JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, String pname) { - if (schemaNode.get("message") != null && schemaNode.get("message").get(pname) != null) { - return schemaNode.get("message"); - } - JsonNode messageNode; - messageNode = schemaNode.get("message"); - if (messageNode == null && parentSchema != null) { - messageNode = parentSchema.schemaNode.get("message"); - if (messageNode == null) { - return getMessageNode(parentSchema.schemaNode, parentSchema.getParentSchema(), pname); - } - } - return messageNode; - } - /************************ START OF VALIDATE METHODS **********************************/ @Override - public Set validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) { SchemaValidatorsConfig config = this.validationContext.getConfig(); - Set errors = new LinkedHashSet<>(); + Set errors = null; // Get the collector context. CollectorContext collectorContext = executionContext.getCollectorContext(); // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); - for (JsonValidator v : getValidators().values()) { - Set results = Collections.emptySet(); + for (JsonValidator v : getValidators()) { + Set results = null; Scope parentScope = collectorContext.enterDynamicScope(this); try { - results = v.validate(executionContext, jsonNode, rootNode, at); + results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); } finally { Scope scope = collectorContext.exitDynamicScope(); - if (results.isEmpty()) { + if (results == null || results.isEmpty()) { parentScope.mergeWith(scope); } else { + if (errors == null) { + errors = new LinkedHashSet<>(); + } errors.addAll(results); if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { @@ -432,12 +369,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (null != discriminatorContext) { final ObjectNode discriminatorToUse; final ObjectNode discriminatorFromContext = discriminatorContext - .getDiscriminatorForPath(this.schemaPath); + .getDiscriminatorForPath(this.schemaLocation); if (null == discriminatorFromContext) { // register the current discriminator. This can only happen when the current context discriminator // was not registered via allOf. In that case we have a $ref to the schema with discriminator that gets // used for validation before allOf validation has kicked in - discriminatorContext.registerDiscriminator(this.schemaPath, discriminator); + discriminatorContext.registerDiscriminator(this.schemaLocation, discriminator); discriminatorToUse = discriminator; } else { discriminatorToUse = discriminatorFromContext; @@ -453,7 +390,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - return errors; + return errors == null ? Collections.emptySet() : errors; } /** @@ -464,7 +401,34 @@ public Set validate(ExecutionContext executionContext, JsonNo * list if there is no error. */ public Set validate(JsonNode rootNode) { - return validate(createExecutionContext(), rootNode); + return validate(rootNode, OutputFormat.DEFAULT); + } + + /** + * Validates the given root JsonNode, starting at the root of the data path. The + * output will be formatted using the formatter specified. + * + * @param the result type + * @param rootNode the root note + * @param format the formatter + * @return the result + */ + public T validate(JsonNode rootNode, OutputFormat format) { + return validate(rootNode, format, null); + } + + /** + * Validates the given root JsonNode, starting at the root of the data path. The + * output will be formatted using the formatter specified. + * + * @param the result type + * @param rootNode the root note + * @param format the formatter + * @param executionCustomizer the execution customizer + * @return the result + */ + public T validate(JsonNode rootNode, OutputFormat format, ExecutionCustomizer executionCustomizer) { + return validate(createExecutionContext(), rootNode, format, executionCustomizer); } public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) { @@ -478,11 +442,11 @@ public ValidationResult validateAndCollect(ExecutionContext executionContext, Js * @param executionContext ExecutionContext * @param jsonNode JsonNode * @param rootNode JsonNode - * @param at String path + * @param instanceLocation JsonNodePath * * @return ValidationResult */ - private ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, String at) { + private ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) { // Get the config. SchemaValidatorsConfig config = this.validationContext.getConfig(); // Get the collector context from the thread local. @@ -490,7 +454,7 @@ private ValidationResult validateAndCollect(ExecutionContext executionContext, J // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); // Validate. - Set errors = validate(executionContext, jsonNode, rootNode, at); + Set errors = validate(executionContext, jsonNode, rootNode, instanceLocation); // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors. if (config.doLoadCollectors()) { // Load all the data from collectors into the context. @@ -525,11 +489,13 @@ public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { return walk(createExecutionContext(), node, shouldValidateSchema); } - public ValidationResult walkAtNode(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - return walkAtNodeInternal(executionContext, node, rootNode, at, shouldValidateSchema); + public ValidationResult walkAtNode(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + return walkAtNodeInternal(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } - private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { // Get the config. SchemaValidatorsConfig config = this.validationContext.getConfig(); // Get the collector context. @@ -537,7 +503,7 @@ private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, J // Set the walkEnabled flag in internal validator state. setValidatorState(executionContext, true, shouldValidateSchema); // Walk through the schema. - Set errors = walk(executionContext, node, rootNode, at, shouldValidateSchema); + Set errors = walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors. if (config.doLoadCollectors()) { // Load all the data from collectors into the context. @@ -549,36 +515,37 @@ private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, J } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { Set validationMessages = new LinkedHashSet<>(); // Walk through all the JSONWalker's. - getValidators().forEach((String schemaPathWithKeyword, JsonSchemaWalker jsonWalker) -> { + getValidators().forEach(jsonWalker -> { + JsonNodePath evaluationPathWithKeyword = jsonWalker.getEvaluationPath(); try { // Call all the pre-walk listeners. If at least one of the pre walk listeners // returns SKIP, then skip the walk. if (this.keywordWalkListenerRunner.runPreWalkListeners(executionContext, - schemaPathWithKeyword, + evaluationPathWithKeyword.getName(-1), node, rootNode, - at, - this.schemaPath, + instanceLocation, + jsonWalker.getEvaluationPath(), + jsonWalker.getSchemaLocation(), this.schemaNode, - this.parentSchema, - this.validationContext, this.validationContext.getJsonSchemaFactory())) { - validationMessages.addAll(jsonWalker.walk(executionContext, node, rootNode, at, shouldValidateSchema)); + this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory())) { + validationMessages.addAll(jsonWalker.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); } } finally { // Call all the post-walk listeners. this.keywordWalkListenerRunner.runPostWalkListeners(executionContext, - schemaPathWithKeyword, + evaluationPathWithKeyword.getName(-1), node, rootNode, - at, - this.schemaPath, + instanceLocation, + jsonWalker.getEvaluationPath(), + jsonWalker.getSchemaLocation(), this.schemaNode, this.parentSchema, - this.validationContext, - this.validationContext.getJsonSchemaFactory(), validationMessages); + this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); } }); @@ -587,22 +554,19 @@ public Set walk(ExecutionContext executionContext, JsonNode n /************************ END OF WALK METHODS **********************************/ - private static void setValidatorState(ExecutionContext executionContext, boolean isWalkEnabled, boolean shouldValidateSchema) { + private static void setValidatorState(ExecutionContext executionContext, boolean isWalkEnabled, + boolean shouldValidateSchema) { // Get the Validator state object storing validation data - CollectorContext collectorContext = executionContext.getCollectorContext(); - Object stateObj = collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); - // if one has not been created, instantiate one - if (stateObj == null) { - ValidatorState state = new ValidatorState(); - state.setWalkEnabled(isWalkEnabled); - state.setValidationEnabled(shouldValidateSchema); - collectorContext.add(ValidatorState.VALIDATOR_STATE_KEY, state); + ValidatorState validatorState = executionContext.getValidatorState(); + if (validatorState == null) { + // If one has not been created, instantiate one + executionContext.setValidatorState(new ValidatorState(isWalkEnabled, shouldValidateSchema)); } } @Override public String toString() { - return "\"" + getSchemaPath() + "\" : " + getSchemaNode().toString(); + return "\"" + getEvaluationPath() + "\" : " + getSchemaNode().toString(); } public boolean hasRequiredValidator() { @@ -614,16 +578,21 @@ public JsonValidator getRequiredValidator() { } public boolean hasTypeValidator() { - return this.typeValidator != null; + return getTypeValidator() != null; } public TypeValidator getTypeValidator() { + // As the validators are lazy loaded the typeValidator is only known if the + // validators are not null + if (this.validators == null) { + getValidators(); + } return this.typeValidator; } - private Map getValidators() { + public List getValidators() { if (this.validators == null) { - this.validators = Collections.unmodifiableMap(read(getSchemaNode())); + this.validators = Collections.unmodifiableList(read(getSchemaNode())); } return this.validators; } @@ -640,7 +609,7 @@ private Map getValidators() { public void initializeValidators() { if (!this.validatorsLoaded) { this.validatorsLoaded = true; - for (final JsonValidator validator : getValidators().values()) { + for (final JsonValidator validator : getValidators()) { validator.preloadJsonSchema(); } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index b6fbd2180..b43b190c2 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -42,8 +42,8 @@ public class JsonSchemaFactory { public static class Builder { - private ObjectMapper objectMapper = new ObjectMapper(); - private YAMLMapper yamlMapper = new YAMLMapper(); + private ObjectMapper objectMapper = null; + private YAMLMapper yamlMapper = null; private String defaultMetaSchemaURI; private final Map uriFactoryMap = new HashMap(); private final Map uriFetcherMap = new HashMap(); @@ -327,20 +327,40 @@ public static Builder builder(final JsonSchemaFactory blueprint) { protected JsonSchema newJsonSchema(final URI schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { final ValidationContext validationContext = createValidationContext(schemaNode); validationContext.setConfig(config); - return doCreate(validationContext, "#", schemaUri, schemaNode, null, false); + return doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), + new JsonNodePath(validationContext.getConfig().getPathType()), schemaUri, schemaNode, null, false); } - - public JsonSchema create(ValidationContext validationContext, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema) { - return doCreate(validationContext, null == schemaPath ? "#" : schemaPath, parentSchema.getCurrentUri(), schemaNode, parentSchema, false); + + public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { + return doCreate(validationContext, + null == schemaLocation ? getSchemaLocation(null, schemaNode, validationContext) : schemaLocation, + evaluationPath, parentSchema.getCurrentUri(), schemaNode, parentSchema, false); } - private JsonSchema doCreate(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { - return JsonSchema.from(validationContext, schemaPath, currentUri, schemaNode, parentSchema, suppressSubSchemaRetrieval); + private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { + return JsonSchema.from(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parentSchema, suppressSubSchemaRetrieval); + } + + /** + * Gets the schema location from the $id or retrieval uri. + * + * @param schemaRetrievalUri the schema retrieval uri + * @param schemaNode the schema json + * @param validationContext the validationContext + * @return the schema location + */ + protected SchemaLocation getSchemaLocation(URI schemaRetrievalUri, JsonNode schemaNode, + ValidationContext validationContext) { + String schemaLocation = validationContext.resolveSchemaId(schemaNode); + if (schemaLocation == null && schemaRetrievalUri != null) { + schemaLocation = schemaRetrievalUri.toString(); + } + return schemaLocation != null ? SchemaLocation.of(schemaLocation) : SchemaLocation.DOCUMENT; } protected ValidationContext createValidationContext(final JsonNode schemaNode) { final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); - return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, null); + return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, null); } private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { @@ -433,15 +453,21 @@ public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig co } final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); - + JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); JsonSchema jsonSchema; if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri)) { + String schemaLocationValue = schemaUri.toString(); + if(!schemaLocationValue.contains("#")) { + schemaLocationValue = schemaLocationValue + "#"; + } + SchemaLocation schemaLocation = SchemaLocation.of(schemaLocationValue); ValidationContext validationContext = new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); - jsonSchema = doCreate(validationContext, "#", mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); } else { final ValidationContext validationContext = createValidationContext(schemaNode); validationContext.setConfig(config); - jsonSchema = doCreate(validationContext, "#", mappedUri, schemaNode, null, false); + jsonSchema = doCreate(validationContext, SchemaLocation.DOCUMENT, evaluationPath, mappedUri, + schemaNode, null, false); } if (enableUriSchemaCache) { diff --git a/src/main/java/com/networknt/schema/JsonSchemaRef.java b/src/main/java/com/networknt/schema/JsonSchemaRef.java index c07dc3e26..77b3d2114 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaRef.java +++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java @@ -34,15 +34,17 @@ public JsonSchemaRef(JsonSchema schema) { this.schema = schema; } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - return schema.validate(executionContext, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation) { + return schema.validate(executionContext, node, rootNode, instanceLocation); } public JsonSchema getSchema() { return schema; } - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - return schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); - } + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + return schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + } } diff --git a/src/main/java/com/networknt/schema/JsonValidator.java b/src/main/java/com/networknt/schema/JsonValidator.java index 9040211f4..fe4b71aed 100644 --- a/src/main/java/com/networknt/schema/JsonValidator.java +++ b/src/main/java/com/networknt/schema/JsonValidator.java @@ -16,7 +16,7 @@ package com.networknt.schema; -import java.util.LinkedHashSet; +import java.util.Collections; import java.util.Set; import com.fasterxml.jackson.databind.JsonNode; @@ -26,31 +26,19 @@ * Standard json validator interface, implemented by all validators and JsonSchema. */ public interface JsonValidator extends JsonSchemaWalker { - - /** - * Validate the given root JsonNode, starting at the root of the data path. - * @param executionContext ExecutionContext - * @param rootNode JsonNode - * - * @return A list of ValidationMessage if there is any validation error, or an empty - * list if there is no error. - */ - default Set validate(ExecutionContext executionContext, JsonNode rootNode) { - return validate(executionContext, rootNode, rootNode, PathType.DEFAULT.getRoot()); // TODO: This is not valid when using JSON Pointer. - } - /** * 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 at String + * @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, String at); + Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation); /** * In case the {@link com.networknt.schema.JsonValidator} has a related {@link com.networknt.schema.JsonSchema} or several @@ -69,12 +57,35 @@ default void preloadJsonSchema() throws JsonSchemaException { * validate method if shouldValidateSchema is enabled. */ @Override - default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - Set validationMessages = new LinkedHashSet(); - if (shouldValidateSchema) { - validationMessages = validate(executionContext, node, rootNode, at); - } - return validationMessages; + default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + 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 + */ + public 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 + */ + public JsonNodePath getEvaluationPath(); + + /** + * The keyword of the validator. + * + * @return the keyword + */ + public String getKeyword(); } diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java index 749d01387..bf9b4c45a 100644 --- a/src/main/java/com/networknt/schema/Keyword.java +++ b/src/main/java/com/networknt/schema/Keyword.java @@ -16,17 +16,11 @@ package com.networknt.schema; - -import java.util.Map; - import com.fasterxml.jackson.databind.JsonNode; public interface Keyword { String getValue(); - default void setCustomMessage(Map message) { - //setCustom message - } - - JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception; + JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception; } diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index 9990771b3..46db032c5 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -30,25 +30,24 @@ public class MaxItemsValidator extends BaseJsonValidator implements JsonValidato private int max = 0; - public MaxItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS, validationContext); + public MaxItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_ITEMS, validationContext); if (schemaNode.canConvertToExactIntegral()) { max = schemaNode.intValue(); } this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 > max) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); } } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index 5a9330178..55535418e 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -28,18 +28,17 @@ public class MaxLengthValidator extends BaseJsonValidator implements JsonValidat private int maxLength; - public MaxLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_LENGTH, validationContext); + public MaxLengthValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_LENGTH, validationContext); maxLength = Integer.MAX_VALUE; if (schemaNode != null && schemaNode.canConvertToExactIntegral()) { maxLength = schemaNode.intValue(); } this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { @@ -47,8 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + maxLength)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(maxLength).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index ce6b6acc6..56eadd1f1 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -28,23 +28,21 @@ public class MaxPropertiesValidator extends BaseJsonValidator implements JsonVal private int max; - public MaxPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + public MaxPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_PROPERTIES, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_PROPERTIES, validationContext); if (schemaNode.canConvertToExactIntegral()) { max = schemaNode.intValue(); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isObject()) { if (node.size() > max) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + max)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index b3328c21e..e05e53fa6 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -36,8 +36,8 @@ public class MaximumValidator extends BaseJsonValidator { private final ThresholdMixin typedMaximum; - public MaximumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAXIMUM, validationContext); + public MaximumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAXIMUM, validationContext); this.validationContext = validationContext; if (!schemaNode.isNumber()) { throw new JsonSchemaException("maximum value is not a number"); @@ -48,8 +48,6 @@ public MaximumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema excludeEqual = exclusiveMaximumNode.booleanValue(); } - parseErrorCode(getValidatorType().getErrorCodeKey()); - final String maximumText = schemaNode.asText(); if ((schemaNode.isLong() || schemaNode.isInt()) && (JsonType.INTEGER.toString().equals(getNodeFieldType()))) { // "integer", and within long range @@ -107,8 +105,8 @@ public String thresholdValue() { } } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!JsonNodeUtil.isNumber(node, this.validationContext.getConfig())) { // maximum only applies to numbers @@ -116,8 +114,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), typedMaximum.thresholdValue())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java new file mode 100644 index 000000000..69555763f --- /dev/null +++ b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java @@ -0,0 +1,88 @@ +/* + * 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.Map; +import java.util.function.Consumer; + +import com.networknt.schema.i18n.MessageSource; + +public class MessageSourceValidationMessage { + + public static Builder builder(MessageSource messageSource, Map errorMessage, + Consumer observer) { + return new Builder(messageSource, errorMessage, observer); + } + + public static class Builder extends BuilderSupport { + public Builder(MessageSource messageSource, Map errorMessage, + Consumer observer) { + super(messageSource, errorMessage, observer); + } + + @Override + public Builder self() { + return this; + } + } + + public abstract static class BuilderSupport extends ValidationMessage.BuilderSupport { + private final Consumer observer; + private final MessageSource messageSource; + private final Map errorMessage; + private Locale locale; + + public BuilderSupport(MessageSource messageSource, Map errorMessage, + Consumer observer) { + this.messageSource = messageSource; + this.observer = observer; + this.errorMessage = errorMessage; + } + + @Override + public ValidationMessage build() { + // Use custom error message if present + String messagePattern = null; + if (this.errorMessage != null) { + messagePattern = this.errorMessage.get(""); + if (this.property != null) { + String specificMessagePattern = this.errorMessage.get(this.property); + if (specificMessagePattern != null) { + messagePattern = specificMessagePattern; + } + } + this.message = messagePattern; + } + + // Default to message source formatter + if (this.message == null && this.messageSupplier == null && this.messageFormatter == null) { + this.messageFormatter = args -> this.messageSource.getMessage(this.messageKey, + this.locale == null ? Locale.ROOT : this.locale, args); + } + ValidationMessage validationMessage = super.build(); + if (this.observer != null) { + this.observer.accept(validationMessage); + } + return validationMessage; + } + + public S locale(Locale locale) { + this.locale = locale; + return self(); + } + } +} diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index d18c0a655..e7811c8a9 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -28,27 +28,26 @@ public class MinItemsValidator extends BaseJsonValidator implements JsonValidato private int min = 0; - public MinItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_ITEMS, validationContext); + public MinItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_ITEMS, validationContext); if (schemaNode.canConvertToExactIntegral()) { min = schemaNode.intValue(); } this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isArray()) { if (node.size() < min) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, node.size()).build()); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, 1).build()); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 5965c89e9..7038a2bd1 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -28,18 +28,16 @@ public class MinLengthValidator extends BaseJsonValidator implements JsonValidat private int minLength; - public MinLengthValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_LENGTH, validationContext); + public MinLengthValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_LENGTH, validationContext); minLength = Integer.MIN_VALUE; if (schemaNode != null && schemaNode.canConvertToExactIntegral()) { minLength = schemaNode.intValue(); } - this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { @@ -48,8 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + minLength)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(minLength).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index 791b2f9a4..419ca6f1c 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -17,9 +17,9 @@ public class MinMaxContainsValidator extends BaseJsonValidator { private final Set analysis; - public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + public MinMaxContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_CONTAINS, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_CONTAINS, validationContext); Set analysis = null; int min = 1; @@ -31,7 +31,7 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem if (analysis == null) { analysis = new LinkedHashSet<>(); } - analysis.add(new Analysis("minContains", schemaPath)); + analysis.add(new Analysis("minContains", schemaLocation)); } else { min = minNode.intValue(); } @@ -43,7 +43,7 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem if (analysis == null) { analysis = new LinkedHashSet<>(); } - analysis.add(new Analysis("maxContains", schemaPath)); + analysis.add(new Analysis("maxContains", schemaLocation)); } else { max = maxNode.intValue(); } @@ -53,17 +53,18 @@ public MinMaxContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchem if (analysis == null) { analysis = new LinkedHashSet<>(); } - analysis.add(new Analysis("minContainsVsMaxContains", schemaPath)); + analysis.add(new Analysis("minContainsVsMaxContains", schemaLocation)); } this.analysis = analysis; } @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - String at) { + JsonNodePath instanceLocation) { return this.analysis != null ? this.analysis.stream() - .map(analysis -> buildValidationMessage(null, analysis.getAt(), - analysis.getMessageKey(), executionContext.getExecutionConfig().getLocale(), parentSchema.getSchemaNode().toString())) + .map(analysis -> message().instanceLocation(analysis.getSchemaLocation().getFragment()) + .messageKey(analysis.getMessageKey()).locale(executionContext.getExecutionConfig().getLocale()) + .arguments(parentSchema.getSchemaNode().toString()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet(); } @@ -72,17 +73,17 @@ public String getMessageKey() { return messageKey; } - public String getAt() { - return at; + public SchemaLocation getSchemaLocation() { + return schemaLocation; } private final String messageKey; - private final String at; + private final SchemaLocation schemaLocation; - public Analysis(String messageKey, String at) { + public Analysis(String messageKey, SchemaLocation schemaLocation) { super(); this.messageKey = messageKey; - this.at = at; + this.schemaLocation = schemaLocation; } } } diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index d23b573e6..80e41f798 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -28,23 +28,21 @@ public class MinPropertiesValidator extends BaseJsonValidator implements JsonVal protected int min; - public MinPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + public MinPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_PROPERTIES, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_PROPERTIES, validationContext); if (schemaNode.canConvertToExactIntegral()) { min = schemaNode.intValue(); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isObject()) { if (node.size() < min) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), "" + min)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(min).build()); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 2e0016d0a..1b91da2ff 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -39,8 +39,8 @@ public class MinimumValidator extends BaseJsonValidator { */ private final ThresholdMixin typedMinimum; - public MinimumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MINIMUM, validationContext); + public MinimumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MINIMUM, validationContext); if (!schemaNode.isNumber()) { throw new JsonSchemaException("minimum value is not a number"); @@ -51,8 +51,6 @@ public MinimumValidator(String schemaPath, final JsonNode schemaNode, JsonSchema excludeEqual = exclusiveMinimumNode.booleanValue(); } - parseErrorCode(getValidatorType().getErrorCodeKey()); - final String minimumText = schemaNode.asText(); if ((schemaNode.isLong() || schemaNode.isInt()) && JsonType.INTEGER.toString().equals(getNodeFieldType())) { // "integer", and within long range @@ -114,8 +112,8 @@ public String thresholdValue() { this.validationContext = validationContext; } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!JsonNodeUtil.isNumber(node, this.validationContext.getConfig())) { // minimum only applies to numbers @@ -123,8 +121,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), typedMinimum.thresholdValue())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 383edd823..7d1d73f45 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -29,18 +29,15 @@ public class MultipleOfValidator extends BaseJsonValidator implements JsonValida private double divisor = 0; - public MultipleOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.MULTIPLE_OF, validationContext); + public MultipleOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MULTIPLE_OF, validationContext); if (schemaNode.isNumber()) { divisor = schemaNode.doubleValue(); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (node.isNumber()) { double nodeValue = node.doubleValue(); @@ -49,8 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue)); BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { - return Collections.singleton(buildValidationMessage(null, - at, executionContext.getExecutionConfig().getLocale(), "" + divisor)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(divisor).build()); } } } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index f0c239db9..45f8cfc8d 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -27,8 +27,12 @@ public class NonValidationKeyword extends AbstractKeyword { private static final class Validator extends AbstractJsonValidator { + public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword) { + super(schemaLocation, evaluationPath, keyword); + } + @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { return Collections.emptySet(); } } @@ -38,8 +42,8 @@ public NonValidationKeyword(String keyword) { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext) throws JsonSchemaException, Exception { - return new Validator(); + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { + return new Validator(schemaLocation, evaluationPath, this); } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index a53c4db94..c6c4e80fe 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -27,33 +27,34 @@ public class NotAllowedValidator extends BaseJsonValidator implements JsonValida private List fieldNames = new ArrayList(); - public NotAllowedValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT_ALLOWED, validationContext); + public NotAllowedValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.NOT_ALLOWED, validationContext); if (schemaNode.isArray()) { int size = schemaNode.size(); for (int i = 0; i < size; i++) { fieldNames.add(schemaNode.get(i).asText()); } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); - Set errors = new LinkedHashSet(); + Set errors = null; for (String fieldName : fieldNames) { JsonNode propertyNode = node.get(fieldName); if (propertyNode != null) { - errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.add(message().property(fieldName).instanceLocation(instanceLocation.append(fieldName)) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); } } - return Collections.unmodifiableSet(errors); + return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 89f18990e..6e5164301 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -29,25 +29,24 @@ public class NotValidator extends BaseJsonValidator { private final JsonSchema schema; - public NotValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext); - this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); - - parseErrorCode(getValidatorType().getErrorCodeKey()); + public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext); + this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new HashSet<>(); Scope parentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); - errors = this.schema.validate(executionContext, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); + errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(null, - at, executionContext.getExecutionConfig().getLocale(), this.schema.toString())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) + .build()); } return Collections.emptySet(); } finally { @@ -59,15 +58,16 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (shouldValidateSchema) { - return validate(executionContext, node, rootNode, at); + return validate(executionContext, node, rootNode, instanceLocation); } - Set errors = this.schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); + Set errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); if (errors.isEmpty()) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), this.schema.toString())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 068a2e51c..e7e8eb7b4 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -29,26 +29,25 @@ public class OneOfValidator extends BaseJsonValidator { private final List schemas = new ArrayList<>(); - public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext); + public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext); int size = schemaNode.size(); for (int i = 0; i < size; i++) { JsonNode childNode = schemaNode.get(i); - this.schemas.add(validationContext.newSchema( schemaPath + "/" + i, childNode, parentSchema)); + this.schemas.add(validationContext.newSchema( schemaLocation.append(i), evaluationPath.append(i), childNode, parentSchema)); } - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { Set errors = new LinkedHashSet<>(); CollectorContext collectorContext = executionContext.getCollectorContext(); Scope grandParentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); // this is a complex validator, we set the flag to true state.setComplexValidator(true); @@ -65,9 +64,9 @@ public Set validate(ExecutionContext executionContext, JsonNo state.setMatchedNode(true); if (!state.isWalkEnabled()) { - schemaErrors = schema.validate(executionContext, node, rootNode, at); + schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation); } else { - schemaErrors = schema.walk(executionContext, node, rootNode, at, state.isValidationEnabled()); + schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation, state.isValidationEnabled()); } // check if any validation errors have occurred @@ -95,8 +94,9 @@ public Set validate(ExecutionContext executionContext, JsonNo // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. if (numberOfValidSchema != 1) { - ValidationMessage message = buildValidationMessage(null, - at, executionContext.getExecutionConfig().getLocale(), Integer.toString(numberOfValidSchema)); + ValidationMessage message = message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(Integer.toString(numberOfValidSchema)).build(); if (this.failFast) { throw new JsonSchemaException(message); } @@ -111,7 +111,7 @@ public Set validate(ExecutionContext executionContext, JsonNo state.setMatchedNode(true); // reset the ValidatorState object - resetValidatorState(collectorContext); + resetValidatorState(executionContext); return Collections.unmodifiableSet(errors); } finally { @@ -122,20 +122,20 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - private static void resetValidatorState(CollectorContext collectorContext) { - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + private static void resetValidatorState(ExecutionContext executionContext) { + ValidatorState state = executionContext.getValidatorState(); state.setComplexValidator(false); state.setMatchedNode(true); } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { HashSet validationMessages = new LinkedHashSet<>(); if (shouldValidateSchema) { - validationMessages.addAll(validate(executionContext, node, rootNode, at)); + validationMessages.addAll(validate(executionContext, node, rootNode, instanceLocation)); } else { for (JsonSchema schema : this.schemas) { - schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); + schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } } return validationMessages; diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java new file mode 100644 index 000000000..840445218 --- /dev/null +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -0,0 +1,127 @@ +/* + * 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.Set; + +/** + * Formats the validation results. + * + * @param the result type + */ +public interface OutputFormat { + /** + * Customize the execution context before validation. + *

+ * The validation context should only be used for reference as it is shared. + * + * @param executionContext the execution context + * @param validationContext the validation context for reference + */ + default void customize(ExecutionContext executionContext, ValidationContext validationContext) { + } + + /** + * Formats the validation results. + * + * @param validationMessages + * @param executionContext + * @return the result + */ + T format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext); + + /** + * The Default output format. + */ + public static final Default DEFAULT = new Default(); + + /** + * The Boolean output format. + */ + public static final Flag BOOLEAN = new Flag(); + + /** + * The Flag output format. + */ + public static final Flag FLAG = new Flag(); + + /** + * The Default output format. + */ + public static class Default implements OutputFormat> { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + } + + @Override + public Set format(Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { + return validationMessages; + } + } + + /** + * The Flag output format. + */ + public static class Flag implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + } + + @Override + public FlagOutput format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + return new FlagOutput(validationMessages.isEmpty()); + } + } + + /** + * The Boolean output format. + */ + public static class Boolean implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + } + + @Override + public java.lang.Boolean format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + return validationMessages.isEmpty(); + } + } + + /** + * The Flag output results. + */ + public static class FlagOutput { + private final boolean valid; + + public FlagOutput(boolean valid) { + this.valid = valid; + } + + public boolean isValid() { + return this.valid; + } + } +} diff --git a/src/main/java/com/networknt/schema/PathType.java b/src/main/java/com/networknt/schema/PathType.java index 3142c296f..4104c77d3 100644 --- a/src/main/java/com/networknt/schema/PathType.java +++ b/src/main/java/com/networknt/schema/PathType.java @@ -1,6 +1,6 @@ package com.networknt.schema; -import java.util.function.Function; +import java.util.function.BiFunction; import java.util.function.IntPredicate; /** @@ -11,12 +11,13 @@ public enum PathType { /** * The legacy approach, loosely based on JSONPath (but not guaranteed to give valid JSONPath expressions). */ - LEGACY("$", (token) -> "." + replaceCommonSpecialCharactersIfPresent(token), (index) -> "[" + index + "]"), + LEGACY("$", (currentPath, token) -> currentPath + "." + replaceCommonSpecialCharactersIfPresent(token), + (currentPath, index) -> currentPath + "[" + index + "]"), /** * Paths as JSONPath expressions. */ - JSON_PATH("$", (token) -> { + JSON_PATH("$", (currentPath, token) -> { if (token.isEmpty()) { throw new IllegalArgumentException("A JSONPath selector cannot be empty"); @@ -31,7 +32,7 @@ public enum PathType { * - any non-ASCII Unicode character */ if (JSONPath.isShorthand(token)) { - return "." + token; + return currentPath + "." + token; } // Replace single quote (used to wrap property names when not shorthand form. @@ -39,13 +40,13 @@ public enum PathType { // Replace other special characters. token = replaceCommonSpecialCharactersIfPresent(token); - return "['" + token + "']"; - }, (index) -> "[" + index + "]"), + return currentPath + "['" + token + "']"; + }, (currentPath, index) -> currentPath + "[" + index + "]"), /** * Paths as JSONPointer expressions. */ - JSON_POINTER("", (token) -> { + JSON_POINTER("", (currentPath, token) -> { /* * Escape '~' with '~0' and '/' with '~1'. */ @@ -53,16 +54,23 @@ public enum PathType { if (token.indexOf('/') != -1) token = token.replace("/", "~1"); // Replace other special characters. token = replaceCommonSpecialCharactersIfPresent(token); - return "/" + token; - }, (index) -> "/" + index); + return currentPath + "/" + token; + }, (currentPath, index) -> currentPath + "/" + index), + + /** + * Paths as a URI reference. + */ + URI_REFERENCE("", (currentPath, token) -> { + return !currentPath.isEmpty() ? currentPath + "/" + token : token; + }, (currentPath, index) -> currentPath + "/" + index); /** * The default path generation approach to use. */ public static final PathType DEFAULT = LEGACY; private final String rootToken; - private final Function appendTokenFn; - private final Function appendIndexFn; + private final BiFunction appendTokenFn; + private final BiFunction appendIndexFn; /** * Constructor. @@ -71,7 +79,7 @@ public enum PathType { * @param appendTokenFn A function used to define the path fragment used to append a token (e.g. property) to an existing path. * @param appendIndexFn A function used to append an index (for arrays) to an existing path. */ - PathType(String rootToken, Function appendTokenFn, Function appendIndexFn) { + PathType(String rootToken, BiFunction appendTokenFn, BiFunction appendIndexFn) { this.rootToken = rootToken; this.appendTokenFn = appendTokenFn; this.appendIndexFn = appendIndexFn; @@ -100,7 +108,7 @@ private static String replaceCommonSpecialCharactersIfPresent(String token) { * @return The resulting complete path. */ public String append(String currentPath, String child) { - return currentPath + this.appendTokenFn.apply(child); + return this.appendTokenFn.apply(currentPath, child); } /** @@ -111,7 +119,7 @@ public String append(String currentPath, String child) { * @return The resulting complete path. */ public String append(String currentPath, int index) { - return currentPath + this.appendIndexFn.apply(index); + return this.appendIndexFn.apply(currentPath, index); } /** diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index c90fd92cf..ceeb6c2b8 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -28,9 +28,9 @@ public class PatternPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(PatternPropertiesValidator.class); private final Map schemas = new IdentityHashMap<>(); - public PatternPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, + public PatternPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES, validationContext); if (!schemaNode.isObject()) { throw new JsonSchemaException("patternProperties must be an object node"); } @@ -38,34 +38,40 @@ public PatternPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSc while (names.hasNext()) { String name = names.next(); RegularExpression pattern = RegularExpression.compile(name, validationContext); - schemas.put(pattern, validationContext.newSchema(name, schemaNode.get(name), parentSchema)); + schemas.put(pattern, validationContext.newSchema(schemaLocation.append(name), evaluationPath.append(name), + schemaNode.get(name), parentSchema)); } } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); - - Set errors = new LinkedHashSet(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!node.isObject()) { - return errors; + return Collections.emptySet(); } - + Set errors = null; Iterator names = node.fieldNames(); while (names.hasNext()) { String name = names.next(); JsonNode n = node.get(name); for (Map.Entry entry : schemas.entrySet()) { if (entry.getKey().matches(name)) { - Set results = entry.getValue().validate(executionContext, n, rootNode, atPath(at, name)); + JsonNodePath path = instanceLocation.append(name); + Set results = entry.getValue().validate(executionContext, n, rootNode, path); if (results.isEmpty()) { - executionContext.getCollectorContext().getEvaluatedProperties().add(atPath(at, name)); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + executionContext.getCollectorContext().getEvaluatedProperties().add(path); + } + } else { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(results); } - errors.addAll(results); } } } - return Collections.unmodifiableSet(errors); + return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @Override diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index 362070391..c29fe086b 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -30,8 +30,8 @@ public class PatternValidator extends BaseJsonValidator { private String pattern; private RegularExpression compiledPattern; - public PatternValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN, validationContext); + public PatternValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN, validationContext); this.pattern = Optional.ofNullable(schemaNode).filter(JsonNode::isTextual).map(JsonNode::textValue).orElse(null); try { @@ -42,7 +42,6 @@ public PatternValidator(String schemaPath, JsonNode schemaNode, JsonSchema paren throw e; } this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } private boolean matches(String value) { @@ -50,8 +49,8 @@ private boolean matches(String value) { } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { @@ -60,13 +59,13 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { - return Collections.singleton( - buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), this.pattern)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.pattern).build()); } } catch (JsonSchemaException e) { throw e; } catch (RuntimeException e) { - logger.error("Failed to apply pattern '{}' at {}: {}", this.pattern, at, e.getMessage()); + logger.error("Failed to apply pattern '{}' at {}: {}", this.pattern, instanceLocation, e.getMessage()); throw e; } diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index c3362254f..4670f11bb 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -36,14 +36,14 @@ public class PrefixItemsValidator extends BaseJsonValidator { private final List tupleSchema; private WalkListenerRunner arrayItemWalkListenerRunner; - public PrefixItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PREFIX_ITEMS, validationContext); + public PrefixItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PREFIX_ITEMS, validationContext); this.tupleSchema = new ArrayList<>(); if (schemaNode instanceof ArrayNode && 0 < schemaNode.size()) { for (JsonNode s : schemaNode) { - this.tupleSchema.add(validationContext.newSchema(schemaPath, s, parentSchema)); + this.tupleSchema.add(validationContext.newSchema(schemaLocation, evaluationPath, s, parentSchema)); } } else { throw new IllegalArgumentException("The value of 'prefixItems' MUST be a non-empty array of valid JSON Schemas."); @@ -52,83 +52,88 @@ public PrefixItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema p this.arrayItemWalkListenerRunner = new DefaultItemWalkListenerRunner(validationContext.getConfig().getArrayItemWalkListeners()); this.validationContext = validationContext; - - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); - Set errors = new LinkedHashSet<>(); - + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); // ignores non-arrays if (node.isArray()) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); - for (int i = 0; i < Math.min(node.size(), this.tupleSchema.size()); ++i) { - String path = atPath(at, i); + Set errors = new LinkedHashSet<>(); + Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + int count = Math.min(node.size(), this.tupleSchema.size()); + for (int i = 0; i < count; ++i) { + JsonNodePath path = instanceLocation.append(i); Set results = this.tupleSchema.get(i).validate(executionContext, node.get(i), rootNode, path); if (results.isEmpty()) { - evaluatedItems.add(path); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + evaluatedItems.add(path); + } } else { errors.addAll(results); } } + return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); + } else { + return Collections.emptySet(); } - - return Collections.unmodifiableSet(errors); } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { Set validationMessages = new LinkedHashSet<>(); if (this.applyDefaultsStrategy.shouldApplyArrayDefaults() && node.isArray()) { ArrayNode array = (ArrayNode) node; - for (int i = 0; i < Math.min(node.size(), this.tupleSchema.size()); ++i) { + int count = Math.min(node.size(), this.tupleSchema.size()); + for (int i = 0; i < count; ++i) { JsonNode n = node.get(i); JsonNode defaultNode = this.tupleSchema.get(i).getSchemaNode().get("default"); if (n.isNull() && defaultNode != null) { array.set(i, defaultNode); n = defaultNode; } - doWalk(executionContext, validationMessages, i, n, rootNode, at, shouldValidateSchema); + doWalk(executionContext, validationMessages, i, n, rootNode, instanceLocation, shouldValidateSchema); } } return validationMessages; } - private void doWalk(ExecutionContext executionContext, Set validationMessages, int i, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, atPath(at, i), shouldValidateSchema, validationMessages); + private void doWalk(ExecutionContext executionContext, Set validationMessages, int i, + JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { + walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, instanceLocation.append(i), + shouldValidateSchema, validationMessages); } - private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema, Set validationMessages) { + private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema, Set validationMessages) { //@formatter:off boolean executeWalk = this.arrayItemWalkListenerRunner.runPreWalkListeners( executionContext, ValidatorTypeCode.PREFIX_ITEMS.getValue(), node, rootNode, - at, - walkSchema.getSchemaPath(), + instanceLocation, + walkSchema.getEvaluationPath(), + walkSchema.getSchemaLocation(), walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), - this.validationContext, this.validationContext.getJsonSchemaFactory() + walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory() ); if (executeWalk) { - validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, at, shouldValidateSchema)); + validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); } this.arrayItemWalkListenerRunner.runPostWalkListeners( executionContext, ValidatorTypeCode.PREFIX_ITEMS.getValue(), node, rootNode, - at, - walkSchema.getSchemaPath(), + instanceLocation, + this.evaluationPath, + walkSchema.getSchemaLocation(), walkSchema.getSchemaNode(), walkSchema.getParentSchema(), - this.validationContext, - this.validationContext.getJsonSchemaFactory(), validationMessages + this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages ); //@formatter:on } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 31003284e..67ffcb34f 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -31,32 +31,38 @@ public class PropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(PropertiesValidator.class); private final Map schemas = new LinkedHashMap<>(); - public PropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); + public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); this.validationContext = validationContext; for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { String pname = it.next(); - this.schemas.put(pname, validationContext.newSchema(schemaPath + "/" + pname, schemaNode.get(pname), parentSchema)); + this.schemas.put(pname, validationContext.newSchema(schemaLocation.append(pname), + evaluationPath.append(pname), schemaNode.get(pname), parentSchema)); } } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(this.validationContext.getConfig().getPropertyWalkListeners()); - Set errors = new LinkedHashSet<>(); + Set errors = null; // get the Validator state object storing validation data - ValidatorState state = (ValidatorState) collectorContext.get(ValidatorState.VALIDATOR_STATE_KEY); + ValidatorState state = executionContext.getValidatorState(); + + Set requiredErrors = null; for (Map.Entry entry : this.schemas.entrySet()) { JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = node.get(entry.getKey()); if (propertyNode != null) { - collectorContext.getEvaluatedProperties().add(atPath(at, entry.getKey())); // TODO: This should happen after validation + JsonNodePath path = instanceLocation.append(entry.getKey()); + if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { + collectorContext.getEvaluatedProperties().add(path); // TODO: This should happen after validation + } // check whether this is a complex validator. save the state boolean isComplex = state.isComplexValidator(); // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated @@ -68,10 +74,19 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!state.isWalkEnabled()) { //validate the child element(s) - errors.addAll(propertySchema.validate(executionContext, propertyNode, rootNode, atPath(at, entry.getKey()))); + Set result = propertySchema.validate(executionContext, propertyNode, rootNode, path); + if (!result.isEmpty()) { + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(result); + } } else { // check if walker is enabled. If it is enabled it is upto the walker implementation to decide about the validation. - walkSchema(executionContext, entry, node, rootNode, at, state.isValidationEnabled(), errors, propertyWalkListenerRunner); + if (errors == null) { + errors = new LinkedHashSet<>(); + } + walkSchema(executionContext, entry, node, rootNode, instanceLocation, state.isValidationEnabled(), errors, propertyWalkListenerRunner); } // reset the complex flag to the original value before the recursive call @@ -83,36 +98,43 @@ public Set validate(ExecutionContext executionContext, JsonNo } else { // check whether the node which has not matched was mandatory or not if (getParentSchema().hasRequiredValidator()) { - Set requiredErrors = getParentSchema().getRequiredValidator().validate(executionContext, node, rootNode, at); - - if (!requiredErrors.isEmpty()) { - // the node was mandatory, decide which behavior to employ when validator has not matched - if (state.isComplexValidator()) { - // this was a complex validator (ex oneOf) and the node has not been matched - state.setMatchedNode(false); - return Collections.emptySet(); + + // The required validator runs for all properties in the node and not just the + // current propertyNode + if (requiredErrors == null) { + requiredErrors = getParentSchema().getRequiredValidator().validate(executionContext, node, rootNode, instanceLocation); + + if (!requiredErrors.isEmpty()) { + // the node was mandatory, decide which behavior to employ when validator has not matched + if (state.isComplexValidator()) { + // this was a complex validator (ex oneOf) and the node has not been matched + state.setMatchedNode(false); + return Collections.emptySet(); + } + if (errors == null) { + errors = new LinkedHashSet<>(); + } + errors.addAll(requiredErrors); } - errors.addAll(requiredErrors); } } } } - - return Collections.unmodifiableSet(errors); + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { HashSet validationMessages = new LinkedHashSet<>(); if (this.applyDefaultsStrategy.shouldApplyPropertyDefaults() && null != node && node.getNodeType() == JsonNodeType.OBJECT) { applyPropertyDefaults((ObjectNode) node); } if (shouldValidateSchema) { - validationMessages.addAll(validate(executionContext, node, rootNode, at)); + validationMessages.addAll(validate(executionContext, node, rootNode, instanceLocation)); } else { WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(this.validationContext.getConfig().getPropertyWalkListeners()); for (Map.Entry entry : this.schemas.entrySet()) { - walkSchema(executionContext, entry, node, rootNode, at, shouldValidateSchema, validationMessages, propertyWalkListenerRunner); + walkSchema(executionContext, entry, node, rootNode, instanceLocation, shouldValidateSchema, validationMessages, propertyWalkListenerRunner); } } return validationMessages; @@ -139,21 +161,23 @@ private static JsonNode getDefaultNode(final Map.Entry entry return propertySchema.getSchemaNode().get("default"); } - private void walkSchema(ExecutionContext executionContext, Map.Entry entry, JsonNode node, JsonNode rootNode, String at, - boolean shouldValidateSchema, Set validationMessages, WalkListenerRunner propertyWalkListenerRunner) { + private void walkSchema(ExecutionContext executionContext, Map.Entry entry, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema, + Set validationMessages, WalkListenerRunner propertyWalkListenerRunner) { JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = (node == null ? null : node.get(entry.getKey())); + JsonNodePath path = instanceLocation.append(entry.getKey()); boolean executeWalk = propertyWalkListenerRunner.runPreWalkListeners(executionContext, - ValidatorTypeCode.PROPERTIES.getValue(), propertyNode, rootNode, atPath(at, entry.getKey()), - propertySchema.getSchemaPath(), propertySchema.getSchemaNode(), propertySchema.getParentSchema(), - this.validationContext, this.validationContext.getJsonSchemaFactory()); + ValidatorTypeCode.PROPERTIES.getValue(), propertyNode, rootNode, path, + propertySchema.getEvaluationPath(), propertySchema.getSchemaLocation(), propertySchema.getSchemaNode(), + propertySchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory()); if (executeWalk) { validationMessages.addAll( - propertySchema.walk(executionContext, propertyNode, rootNode, atPath(at, entry.getKey()), shouldValidateSchema)); + propertySchema.walk(executionContext, propertyNode, rootNode, path, shouldValidateSchema)); } propertyWalkListenerRunner.runPostWalkListeners(executionContext, ValidatorTypeCode.PROPERTIES.getValue(), propertyNode, - rootNode, atPath(at, entry.getKey()), propertySchema.getSchemaPath(), - propertySchema.getSchemaNode(), propertySchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); + rootNode, path, propertySchema.getEvaluationPath(), + propertySchema.getSchemaLocation(), propertySchema.getSchemaNode(), propertySchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); } diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index 765cb17eb..96d9ed3ea 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -29,27 +29,27 @@ public class PropertyNamesValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(PropertyNamesValidator.class); private final JsonSchema innerSchema; - public PropertyNamesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTYNAMES, validationContext); - innerSchema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); + public PropertyNamesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTYNAMES, validationContext); + innerSchema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); Set errors = new LinkedHashSet(); for (Iterator it = node.fieldNames(); it.hasNext(); ) { final String pname = it.next(); final TextNode pnameText = TextNode.valueOf(pname); - final Set schemaErrors = innerSchema.validate(executionContext, pnameText, node, atPath(at, pname)); + final Set schemaErrors = innerSchema.validate(executionContext, pnameText, node, instanceLocation.append(pname)); for (final ValidationMessage schemaError : schemaErrors) { - final String path = schemaError.getPath(); + final String path = schemaError.getInstanceLocation().toString(); String msg = schemaError.getMessage(); if (msg.startsWith(path)) msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - errors.add(buildValidationMessage(pname, - schemaError.getPath(), executionContext.getExecutionConfig().getLocale(), msg)); + errors.add(message().property(pname).instanceLocation(schemaError.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(msg).build()); } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 31518fc59..a3d1e4f22 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -29,19 +29,19 @@ public class ReadOnlyValidator extends BaseJsonValidator { private final boolean readOnly; - public ReadOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY, validationContext); + public ReadOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.READ_ONLY, validationContext); this.readOnly = validationContext.getConfig().isReadOnly(); logger.debug("Loaded ReadOnlyValidator for property {} as {}", parentSchema, "read mode"); - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (this.readOnly) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 869daae1c..ce0d4eba1 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -26,28 +26,28 @@ public class RecursiveRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class); - public RecursiveRefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext); + public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext); String refValue = schemaNode.asText(); if (!"#".equals(refValue)) { ValidationMessage validationMessage = ValidationMessage.builder() .type(ValidatorTypeCode.RECURSIVE_REF.getValue()).code("internal.invalidRecursiveRef") - .message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").path(schemaPath) - .schemaPath(schemaPath).arguments(refValue).build(); + .message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").instanceLocation(schemaLocation.getFragment()) + .evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new HashSet<>(); Scope parentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); JsonSchema schema = collectorContext.getOutermostSchema(); if (null != schema) { @@ -55,7 +55,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. schema.getValidationContext().setConfig(getParentSchema().getValidationContext().getConfig()); - errors = schema.validate(executionContext, node, rootNode, at); + errors = schema.validate(executionContext, node, rootNode, instanceLocation); } } finally { Scope scope = collectorContext.exitDynamicScope(); @@ -68,14 +68,14 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new HashSet<>(); Scope parentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); JsonSchema schema = collectorContext.getOutermostSchema(); if (null != schema) { @@ -83,7 +83,7 @@ public Set walk(ExecutionContext executionContext, JsonNode n // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. schema.getValidationContext().setConfig(getParentSchema().getValidationContext().getConfig()); - errors = schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); + errors = schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } } finally { Scope scope = collectorContext.exitDynamicScope(); diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 0c3175fd6..5f0649a09 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -37,20 +37,22 @@ public class RefValidator extends BaseJsonValidator { private static final String REF_CURRENT = "#"; private static final String URN_SCHEME = URNURIFactory.SCHEME; - public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); + public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); String refValue = schemaNode.asText(); this.parentSchema = parentSchema; - this.schema = getRefSchema(parentSchema, validationContext, refValue); + this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); if (this.schema == null) { ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .path(schemaPath).schemaPath(schemaPath).arguments(refValue).build(); + .instanceLocation(schemaLocation.getFragment()).evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } } - static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue) { + static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, + JsonNodePath evaluationPath) { + // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; JsonSchema parent = parentSchema; @@ -79,7 +81,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } } else if (URN_SCHEME.equals(schemaUri.getScheme())) { // Try to resolve URN schema as a JsonSchemaRef to some sub-schema of the parent - JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, schemaUri.toString(), refValueOriginal); + JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, schemaUri.toString(), refValueOriginal, evaluationPath); if (ref != null) { return ref; } @@ -96,18 +98,33 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val if (refValue.equals(REF_CURRENT)) { return new JsonSchemaRef(parent.findAncestor()); } - return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal); + return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); } private static JsonSchemaRef getJsonSchemaRef(JsonSchema parent, ValidationContext validationContext, String refValue, - String refValueOriginal) { + String refValueOriginal, + JsonNodePath evaluationPath) { JsonNode node = parent.getRefSchemaNode(refValue); if (node != null) { JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal); if (ref == null) { - final JsonSchema schema = validationContext.newSchema(refValue, node, parent); + SchemaLocation path = null; + if (refValue.startsWith(REF_CURRENT)) { + // relative + path = parent.schemaLocation; + JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); + String[] parts = refValue.split("/"); + for (int x = 1; x < parts.length; x++) { + fragment = fragment.append(parts[x]); + } + path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), fragment); + } else { + // absolute + path = SchemaLocation.of(refValue); + } + final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, parent); ref = new JsonSchemaRef(schema); validationContext.setReferenceParsingInProgress(refValueOriginal, ref); } @@ -142,20 +159,20 @@ private static URI determineSchemaUrn(final URNFactory urnFactory, final String } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new HashSet<>(); Scope parentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); if (this.schema != null) { - errors = this.schema.validate(executionContext, node, rootNode, at); + errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); } else { errors = Collections.emptySet(); } @@ -169,20 +186,20 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { CollectorContext collectorContext = executionContext.getCollectorContext(); Set errors = new HashSet<>(); Scope parentScope = collectorContext.enterDynamicScope(); try { - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); if (this.schema != null) { - errors = this.schema.walk(executionContext, node, rootNode, at, shouldValidateSchema); + errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } return errors; } finally { diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 8a340363d..369e913f1 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -27,36 +27,42 @@ public class RequiredValidator extends BaseJsonValidator implements JsonValidato private List fieldNames = new ArrayList(); - public RequiredValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REQUIRED, validationContext); + public RequiredValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REQUIRED, validationContext); if (schemaNode.isArray()) { for (JsonNode fieldNme : schemaNode) { fieldNames.add(fieldNme.asText()); } } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (!node.isObject()) { return Collections.emptySet(); } - Set errors = new LinkedHashSet(); + Set errors = null; for (String fieldName : fieldNames) { JsonNode propertyNode = node.get(fieldName); if (propertyNode == null) { - errors.add(buildValidationMessage(fieldName, at, executionContext.getExecutionConfig().getLocale(), fieldName)); + if (errors == null) { + errors = new LinkedHashSet<>(); + } + /** + * Note that for the required validation the instanceLocation does not contain the missing property + *

+ * @see Basic + */ + errors.add(message().property(fieldName).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); } } - return Collections.unmodifiableSet(errors); + return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } } diff --git a/src/main/java/com/networknt/schema/SchemaLocation.java b/src/main/java/com/networknt/schema/SchemaLocation.java new file mode 100644 index 000000000..c0ecda4d3 --- /dev/null +++ b/src/main/java/com/networknt/schema/SchemaLocation.java @@ -0,0 +1,389 @@ +/* + * 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.Objects; + +/** + * The schema location is the canonical IRI 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. + */ +public class SchemaLocation { + private static final JsonNodePath JSON_POINTER = new JsonNodePath(PathType.JSON_POINTER); + private static final JsonNodePath ANCHOR = new JsonNodePath(PathType.URI_REFERENCE); + + /** + * Represents a relative schema location to the current document. + */ + public static final SchemaLocation DOCUMENT = new SchemaLocation(null, JSON_POINTER); + + private final AbsoluteIri absoluteIri; + private final JsonNodePath fragment; + + private volatile String value = null; // computed lazily + + /** + * Constructs a new {@link SchemaLocation}. + * + * @param absoluteIri canonical absolute IRI of the schema object + * @param fragment the fragment + */ + public SchemaLocation(AbsoluteIri absoluteIri, JsonNodePath fragment) { + this.absoluteIri = absoluteIri; + this.fragment = fragment; + } + + /** + * Constructs a new {@link SchemaLocation}. + * + * @param absoluteIri canonical absolute IRI of the schema object + */ + public SchemaLocation(AbsoluteIri absoluteIri) { + this(absoluteIri, JSON_POINTER); + } + + /** + * Gets the canonical absolute IRI of the schema object. + *

+ * This is a unique identifier indicated by the $id property or id property in + * Draft 4 and earlier. This does not have to be network accessible. + * + * @return the canonical absolute IRI of the schema object. + */ + public AbsoluteIri getAbsoluteIri() { + return this.absoluteIri; + } + + /** + * Gets the fragment. + * + * @return the fragment + */ + public JsonNodePath getFragment() { + return this.fragment; + } + + /** + * Appends the token to the fragment. + * + * @param token the segment + * @return a new schema location with the segment + */ + public SchemaLocation append(String token) { + return new SchemaLocation(this.absoluteIri, this.fragment.append(token)); + } + + /** + * Appends the index to the fragment. + * + * @param index the segment + * @return a new schema location with the segment + */ + public SchemaLocation append(int index) { + return new SchemaLocation(this.absoluteIri, this.fragment.append(index)); + } + + /** + * Parses a string representing an IRI of the schema location. + * + * @param iri the IRI + * @return the schema location + */ + public static SchemaLocation of(String iri) { + if (iri == null) { + return null; + } + if ("#".equals(iri)) { + return DOCUMENT; + } + String[] iriParts = iri.split("#"); + AbsoluteIri absoluteIri = null; + JsonNodePath fragment = JSON_POINTER; + if (iriParts.length > 0) { + absoluteIri = AbsoluteIri.of(iriParts[0]); + } + if (iriParts.length > 1) { + fragment = Fragment.of(iriParts[1]); + } + return new SchemaLocation(absoluteIri, fragment); + } + + /** + * Resolves against a absolute IRI reference or fragment. + * + * @param absoluteIriReferenceOrFragment to resolve + * @return the resolved schema location + */ + public SchemaLocation resolve(String absoluteIriReferenceOrFragment) { + if ("#".equals(absoluteIriReferenceOrFragment)) { + return new SchemaLocation(this.getAbsoluteIri(), JSON_POINTER); + } + JsonNodePath fragment = JSON_POINTER; + String[] parts = absoluteIriReferenceOrFragment.split("#"); + AbsoluteIri absoluteIri = this.getAbsoluteIri(); + if (absoluteIri != null) { + if (!parts[0].isEmpty()) { + absoluteIri = absoluteIri.resolve(parts[0]); + } + } else { + absoluteIri = AbsoluteIri.of(parts[0]); + } + if (parts.length > 1 && !parts[1].isEmpty()) { + fragment = Fragment.of(parts[1]); + } + return new SchemaLocation(absoluteIri, fragment); + } + + /** + * Resolves against a absolute IRI reference or fragment. + * + * @param schemaLocation the parent + * @param absoluteIriReferenceOrFragment to resolve + * @return the resolved schema location + */ + public static String resolve(SchemaLocation schemaLocation, String absoluteIriReferenceOrFragment) { + if ("#".equals(absoluteIriReferenceOrFragment)) { + return schemaLocation.getAbsoluteIri().toString() + "#"; + } + String[] parts = absoluteIriReferenceOrFragment.split("#"); + AbsoluteIri absoluteIri = schemaLocation.getAbsoluteIri(); + String resolved = parts[0]; + if (absoluteIri != null) { + if (!parts[0].isEmpty()) { + resolved = absoluteIri.resolve(parts[0]).toString(); + } else { + resolved = absoluteIri.toString(); + } + } + if (parts.length > 1 && !parts[1].isEmpty()) { + resolved = resolved + "#" + parts[1]; + } else { + resolved = resolved + "#"; + } + return resolved; + } + + /** + * The fragment can be a JSON pointer to the document or an anchor. + */ + public static class Fragment { + /** + * Parses a string representing a fragment. + * + * @param fragmentString + * @return the path + */ + public static JsonNodePath of(String fragmentString) { + if (fragmentString.startsWith("#")) { + fragmentString = fragmentString.substring(1); + } + JsonNodePath fragment = JSON_POINTER; + String[] fragmentParts = fragmentString.split("/"); + + boolean jsonPointer = false; + if (fragmentString.startsWith("/")) { + // json pointer + jsonPointer = true; + } else { + // anchor + fragment = ANCHOR; + } + + int index = -1; + for (int fragmentPartIndex = 0; fragmentPartIndex < fragmentParts.length; fragmentPartIndex++) { + if (fragmentPartIndex == 0 && jsonPointer) { + continue; + } + String fragmentPart = fragmentParts[fragmentPartIndex]; + for (int x = 0; x < fragmentPart.length(); x++) { + char ch = fragmentPart.charAt(x); + if (ch >= '0' && ch <= '9') { + if (x == 0) { + index = 0; + } else { + index = index * 10; + } + index += (ch - '0'); + } else { + index = -1; // Not an index + break; + } + } + if (index != -1) { + fragment = fragment.append(index); + } else { + fragment = fragment.append(fragmentPart.toString()); + } + } + return fragment; + } + + /** + * Determine if the string is a fragment. + * + * @param fragmentString to evaluate + * @return true if it is a fragment + */ + public static boolean isFragment(String fragmentString) { + return fragmentString.startsWith("#"); + } + + /** + * Determine if the string is a JSON Pointer fragment. + * + * @param fragmentString to evaluate + * @return true if it is a JSON Pointer fragment + */ + public static boolean isJsonPointerFragment(String fragmentString) { + return fragmentString.startsWith("#/"); + } + + /** + * Determine if the string is an anchor fragment. + * + * @param fragmentString to evaluate + * @return true if it is an anchor fragment + */ + public static boolean isAnchorFragment(String fragmentString) { + return isFragment(fragmentString) && !isDocumentFragment(fragmentString) + && !isJsonPointerFragment(fragmentString); + } + + /** + * Determine if the string is a fragment referencing the document. + * + * @param fragmentString to evaluate + * @return true if it is a fragment + */ + public static boolean isDocumentFragment(String fragmentString) { + return "#".equals(fragmentString); + } + } + + /** + * Returns a builder for building {@link SchemaLocation}. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for building {@link SchemaLocation}. + */ + public static class Builder { + private AbsoluteIri absoluteIri; + private JsonNodePath fragment = JSON_POINTER; + + /** + * Sets the canonical absolute IRI of the schema object. + *

+ * This is a unique identifier indicated by the $id property or id property in + * Draft 4 and earlier. This does not have to be network accessible. + * + * @param absoluteIri the canonical IRI of the schema object + * @return the builder + */ + protected Builder absoluteIri(AbsoluteIri absoluteIri) { + this.absoluteIri = absoluteIri; + return this; + } + + /** + * Sets the canonical absolute IRI of the schema object. + *

+ * This is a unique identifier indicated by the $id property or id property in + * Draft 4 and earlier. This does not have to be network accessible. + * + * @param absoluteIri the canonical IRI of the schema object + * @return the builder + */ + protected Builder absoluteIri(String absoluteIri) { + return absoluteIri(AbsoluteIri.of(absoluteIri)); + } + + /** + * Sets the fragment. + * + * @param fragment the fragment + * @return the builder + */ + protected Builder fragment(JsonNodePath fragment) { + this.fragment = fragment; + return this; + } + + /** + * Sets the fragment. + * + * @param fragment the fragment + * @return the builder + */ + protected Builder fragment(String fragment) { + return fragment(Fragment.of(fragment)); + } + + /** + * Builds a {@link SchemaLocation}. + * + * @return the schema location + */ + public SchemaLocation build() { + return new SchemaLocation(absoluteIri, fragment); + } + + } + + @Override + public String toString() { + if (this.value == null) { + if (this.absoluteIri != null && this.fragment == null) { + this.value = this.absoluteIri.toString(); + } else { + StringBuilder result = new StringBuilder(); + if (this.absoluteIri != null) { + result.append(this.absoluteIri.toString()); + } + result.append("#"); + if (this.fragment != null) { + result.append(this.fragment.toString()); + } + this.value = result.toString(); + } + } + return this.value; + } + + @Override + public int hashCode() { + return Objects.hash(fragment, absoluteIri); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + SchemaLocation other = (SchemaLocation) obj; + return Objects.equals(fragment, other.fragment) && Objects.equals(absoluteIri, other.absoluteIri); + } +} diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index c6a167b47..3f90acaa4 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -218,11 +218,11 @@ public void setTypeLoose(boolean typeLoose) { } /** - * When enabled, {@link JsonValidator#validate(ExecutionContext, JsonNode, JsonNode, String)} or - * {@link JsonValidator#validate(ExecutionContext, JsonNode)} doesn't return any - * {@link java.util.Set}<{@link ValidationMessage}>, instead a - * {@link JsonSchemaException} is thrown as soon as a validation errors is - * discovered. + * When enabled, + * {@link JsonValidator#validate(ExecutionContext, JsonNode, JsonNode, JsonNodePath)} + * doesn't return any {@link java.util.Set}<{@link ValidationMessage}>, + * instead a {@link JsonSchemaException} is thrown as soon as a validation + * errors is discovered. * * @param failFast boolean */ diff --git a/src/main/java/com/networknt/schema/TrueValidator.java b/src/main/java/com/networknt/schema/TrueValidator.java index 23f981636..908ed483b 100644 --- a/src/main/java/com/networknt/schema/TrueValidator.java +++ b/src/main/java/com/networknt/schema/TrueValidator.java @@ -25,12 +25,12 @@ public class TrueValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(TrueValidator.class); - public TrueValidator(String schemaPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.TRUE, validationContext); + public TrueValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, final JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.TRUE, validationContext); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); // For the true validator, it is always valid which means there is no ValidationMessage. return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 97bcf5d77..ce9930e41 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -30,16 +30,14 @@ public class TypeValidator extends BaseJsonValidator { private JsonSchema parentSchema; private UnionTypeValidator unionTypeValidator; - public TypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); + public TypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); this.schemaType = TypeFactory.getSchemaNodeType(schemaNode); this.parentSchema = parentSchema; this.validationContext = validationContext; if (this.schemaType == JsonType.UNION) { - this.unionTypeValidator = new UnionTypeValidator(schemaPath, schemaNode, parentSchema, validationContext); + this.unionTypeValidator = new UnionTypeValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } public JsonType getSchemaType() { @@ -51,27 +49,28 @@ public boolean equalsToSchemaType(JsonNode node) { } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (this.schemaType == JsonType.UNION) { - return this.unionTypeValidator.validate(executionContext, node, rootNode, at); + return this.unionTypeValidator.validate(executionContext, node, rootNode, instanceLocation); } if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), nodeType.toString(), this.schemaType.toString())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .arguments(nodeType.toString(), this.schemaType.toString()).build()); } // TODO: Is this really necessary? // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} // Hack to catch patternProperties like "^foo":"value" - if (this.schemaPath.endsWith("/type")) { + if (this.schemaLocation.getFragment().getName(-1).equals("type")) { if (rootNode.isArray()) { - executionContext.getCollectorContext().getEvaluatedItems().add(at); + executionContext.getCollectorContext().getEvaluatedItems().add(instanceLocation); } else if (rootNode.isObject()) { - executionContext.getCollectorContext().getEvaluatedProperties().add(at); + executionContext.getCollectorContext().getEvaluatedProperties().add(instanceLocation); } } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 78783e318..415ff5aa9 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -21,34 +21,33 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.stream.Collectors; public class UnevaluatedItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedItemsValidator.class); private final JsonSchema schema; - private final boolean disabled; - public UnevaluatedItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, validationContext); + public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, validationContext); - this.disabled = validationContext.getConfig().isUnevaluatedItemsAnalysisDisabled(); if (schemaNode.isObject() || schemaNode.isBoolean()) { - this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); + this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { throw new IllegalArgumentException("The value of 'unevaluatedItems' MUST be a valid JSON Schema."); } } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - if (this.disabled || !node.isArray()) return Collections.emptySet(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isArray()) return Collections.emptySet(); - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); collectorContext.exitDynamicScope(); try { - Set allPaths = allPaths(node, at); + Set allPaths = allPaths(node, instanceLocation); // Short-circuit since schema is 'true' if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { @@ -56,16 +55,16 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); + Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); // Short-circuit since schema is 'false' if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { return reportUnevaluatedPaths(unevaluatedPaths, executionContext); } - Set failingPaths = new HashSet<>(); + Set failingPaths = new LinkedHashSet<>(); unevaluatedPaths.forEach(path -> { - String pointer = getPathType().convertToJsonPointer(path); + String pointer = path.getPathType().convertToJsonPointer(path.toString()); JsonNode property = rootNode.at(pointer); if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { failingPaths.add(path); @@ -84,25 +83,25 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - private Set allPaths(JsonNode node, String at) { - PathType pathType = getPathType(); - Set collector = new LinkedHashSet<>(); + private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { + Set collector = new LinkedHashSet<>(); int size = node.size(); for (int i = 0; i < size; ++i) { - String path = pathType.append(at, i); + JsonNodePath path = instanceLocation.append(i); collector.add(path); } return collector; } - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - List paths = new ArrayList<>(unevaluatedPaths); - paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { + return unevaluatedPaths + .stream().map(path -> message().instanceLocation(path) + .locale(executionContext.getExecutionConfig().getLocale()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new HashSet<>(allPaths); + private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { + Set unevaluatedProperties = new HashSet<>(allPaths); unevaluatedProperties.removeAll(collectorContext.getEvaluatedItems()); return unevaluatedProperties; } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 79e7517b1..ddd676cd8 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -21,34 +21,33 @@ import org.slf4j.LoggerFactory; import java.util.*; +import java.util.stream.Collectors; public class UnevaluatedPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedPropertiesValidator.class); private final JsonSchema schema; - private final boolean disabled; - public UnevaluatedPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext); + public UnevaluatedPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_PROPERTIES, validationContext); - this.disabled = validationContext.getConfig().isUnevaluatedPropertiesAnalysisDisabled(); if (schemaNode.isObject() || schemaNode.isBoolean()) { - this.schema = validationContext.newSchema(schemaPath, schemaNode, parentSchema); + this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { throw new IllegalArgumentException("The value of 'unevaluatedProperties' MUST be a valid JSON Schema."); } } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - if (this.disabled || !node.isObject()) return Collections.emptySet(); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isObject()) return Collections.emptySet(); - debug(logger, node, rootNode, at); + debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); collectorContext.exitDynamicScope(); try { - Set allPaths = allPaths(node, at); + Set allPaths = allPaths(node, instanceLocation); // Short-circuit since schema is 'true' if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { @@ -56,16 +55,16 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); + Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); // Short-circuit since schema is 'false' if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { return reportUnevaluatedPaths(unevaluatedPaths, executionContext); } - Set failingPaths = new HashSet<>(); + Set failingPaths = new LinkedHashSet<>(); unevaluatedPaths.forEach(path -> { - String pointer = getPathType().convertToJsonPointer(path); + String pointer = path.getPathType().convertToJsonPointer(path.toString()); JsonNode property = rootNode.at(pointer); if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { failingPaths.add(path); @@ -84,24 +83,23 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - private Set allPaths(JsonNode node, String at) { - PathType pathType = getPathType(); - Set collector = new HashSet<>(); + private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { + Set collector = new LinkedHashSet<>(); node.fields().forEachRemaining(entry -> { - String path = pathType.append(at, entry.getKey()); - collector.add(path); + collector.add(instanceLocation.append(entry.getKey())); }); return collector; } - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - List paths = new ArrayList<>(unevaluatedPaths); - paths.sort(String.CASE_INSENSITIVE_ORDER); - return Collections.singleton(buildValidationMessage(null, String.join("\n ", paths), executionContext.getExecutionConfig().getLocale())); + private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { + return unevaluatedPaths + .stream().map(path -> message().instanceLocation(path) + .locale(executionContext.getExecutionConfig().getLocale()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new HashSet<>(allPaths); + private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { + Set unevaluatedProperties = new LinkedHashSet<>(allPaths); unevaluatedProperties.removeAll(collectorContext.getEvaluatedProperties()); return unevaluatedProperties; } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 48997c751..11a566267 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -32,8 +32,8 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat private final String error; - public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext); + public UnionTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext); this.validationContext = validationContext; StringBuilder errorBuilder = new StringBuilder(); @@ -50,9 +50,10 @@ public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema par sep = ", "; if (n.isObject()) - schemas.add(validationContext.newSchema(ValidatorTypeCode.TYPE.getValue(), n, parentSchema)); + schemas.add(validationContext.newSchema(schemaLocation.append(ValidatorTypeCode.TYPE.getValue()), + evaluationPath.append(ValidatorTypeCode.TRUE.getValue()), n, parentSchema)); else - schemas.add(new TypeValidator(schemaPath + "/" + i, n, parentSchema, validationContext)); + schemas.add(new TypeValidator(schemaLocation.append(i), evaluationPath.append(i), n, parentSchema, validationContext)); i++; } @@ -62,15 +63,15 @@ public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema par error = errorBuilder.toString(); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, validationContext.getConfig()); boolean valid = false; for (JsonValidator schema : schemas) { - Set errors = schema.validate(executionContext, node, rootNode, at); + Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); if (errors == null || errors.isEmpty()) { valid = true; break; @@ -78,8 +79,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!valid) { - return Collections.singleton(buildValidationMessage(null, at, - executionContext.getExecutionConfig().getLocale(), nodeType.toString(), error)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(nodeType.toString(), error) + .build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 5d5d3323a..1c7655246 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -29,23 +29,22 @@ public class UniqueItemsValidator extends BaseJsonValidator implements JsonValid private boolean unique = false; - public UniqueItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNIQUE_ITEMS, validationContext); + public UniqueItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNIQUE_ITEMS, validationContext); if (schemaNode.isBoolean()) { unique = schemaNode.booleanValue(); } - - parseErrorCode(getValidatorType().getErrorCodeKey()); } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (unique) { Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).build()); } } } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 83c3cc9ce..e258206b3 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -54,13 +54,13 @@ public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaS this.config = config; } - public JsonSchema newSchema(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema) { - return getJsonSchemaFactory().create(this, schemaPath, schemaNode, parentSchema); + public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { + return getJsonSchemaFactory().create(this, schemaLocation, evaluationPath, schemaNode, parentSchema); } - public JsonValidator newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode, - JsonSchema parentSchema, Map customMessage) { - return this.metaSchema.newValidator(this, schemaPath, keyword, schemaNode, parentSchema, customMessage); + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, + String keyword /* keyword */, JsonNode schemaNode, JsonSchema parentSchema) { + return this.metaSchema.newValidator(this, schemaLocation, evaluationPath, keyword, schemaNode, parentSchema); } public String resolveSchemaId(JsonNode schemaNode) { @@ -105,11 +105,11 @@ public DiscriminatorContext getCurrentDiscriminatorContext() { return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf } - public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") String at) { + public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) { this.discriminatorContexts.push(ctx); } - public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") String at) { + public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") JsonNodePath instanceLocation) { this.discriminatorContexts.pop(); } @@ -127,12 +127,16 @@ public static class DiscriminatorContext { private boolean discriminatorMatchFound = false; - public void registerDiscriminator(final String schemaPath, final ObjectNode discriminator) { - this.discriminators.put(schemaPath, discriminator); + public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) { + this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator); } - public ObjectNode getDiscriminatorForPath(final String schemaPath) { - return this.discriminators.get(schemaPath); + public ObjectNode getDiscriminatorForPath(final SchemaLocation schemaLocation) { + return this.discriminators.get("#" + schemaLocation.getFragment().toString()); + } + + public ObjectNode getDiscriminatorForPath(final String schemaLocation) { + return this.discriminators.get(schemaLocation); } public void markMatch() { diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index bc0a6a520..72d1df861 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -25,23 +25,35 @@ import java.util.Map; import java.util.function.Supplier; +/** + * The output format. + * + * @see JSON + * Schema + */ public class ValidationMessage { private final String type; private final String code; - private final String path; - private final String schemaPath; + private final JsonNodePath evaluationPath; + private final SchemaLocation schemaLocation; + private final JsonNodePath instanceLocation; + private final String property; private final Object[] arguments; private final Map details; private final String messageKey; private final Supplier messageSupplier; - ValidationMessage(String type, String code, String path, String schemaPath, Object[] arguments, - Map details, String messageKey, Supplier messageSupplier) { + ValidationMessage(String type, String code, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNodePath instanceLocation, String property, Object[] arguments, Map details, + String messageKey, Supplier messageSupplier) { super(); this.type = type; this.code = code; - this.path = path; - this.schemaPath = schemaPath; + this.instanceLocation = instanceLocation; + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.property = property; this.arguments = arguments; this.details = details; this.messageKey = messageKey; @@ -53,17 +65,40 @@ public String getCode() { } /** + * The instance location is the location of the JSON value within the root + * instance being validated. + * * @return The path to the input json */ - public String getPath() { - return path; + public JsonNodePath getInstanceLocation() { + return instanceLocation; } /** - * @return The path to the schema + * 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 + */ + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + /** + * The schema location is the canonical IRI 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 */ - public String getSchemaPath() { - return schemaPath; + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + public String getProperty() { + return property; } public Object[] getArguments() { @@ -81,6 +116,10 @@ public String getMessage() { public String getMessageKey() { return messageKey; } + + public boolean isValid() { + return messageSupplier != null; + } @Override public String toString() { @@ -96,8 +135,8 @@ public boolean equals(Object o) { if (type != null ? !type.equals(that.type) : that.type != null) return false; if (code != null ? !code.equals(that.code) : that.code != null) return false; - if (path != null ? !path.equals(that.path) : that.path != null) return false; - if (schemaPath != null ? !schemaPath.equals(that.schemaPath) : that.schemaPath != null) return false; + if (instanceLocation != null ? !instanceLocation.equals(that.instanceLocation) : that.instanceLocation != null) return false; + if (evaluationPath != null ? !evaluationPath.equals(that.evaluationPath) : that.evaluationPath != null) return false; if (details != null ? !details.equals(that.details) : that.details != null) return false; if (messageKey != null ? !messageKey.equals(that.messageKey) : that.messageKey != null) return false; if (!Arrays.equals(arguments, that.arguments)) return false; @@ -108,8 +147,8 @@ public boolean equals(Object o) { public int hashCode() { int result = type != null ? type.hashCode() : 0; result = 31 * result + (code != null ? code.hashCode() : 0); - result = 31 * result + (path != null ? path.hashCode() : 0); - result = 31 * result + (schemaPath != null ? schemaPath.hashCode() : 0); + result = 31 * result + (instanceLocation != null ? instanceLocation.hashCode() : 0); + result = 31 * result + (evaluationPath != null ? evaluationPath.hashCode() : 0); result = 31 * result + (details != null ? details.hashCode() : 0); result = 31 * result + (arguments != null ? Arrays.hashCode(arguments) : 0); result = 31 * result + (messageKey != null ? messageKey.hashCode() : 0); @@ -120,82 +159,105 @@ public String getType() { return type; } - @Deprecated // Use the builder - public static ValidationMessage ofWithCustom(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String customMessage, String at, String schemaPath, Object... arguments) { - ValidationMessage.Builder builder = new ValidationMessage.Builder(); - builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).arguments(arguments) - .format(messageFormat).type(type) - .message(customMessage); - return builder.build(); - } - - @Deprecated // Use the builder - public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Object... arguments) { - return ofWithCustom(type, errorMessageType, messageFormat, errorMessageType.getCustomMessage().get(""), at, schemaPath, arguments); - } - - @Deprecated // Use the builder - public static ValidationMessage of(String type, ErrorMessageType errorMessageType, MessageFormat messageFormat, String at, String schemaPath, Map details) { - ValidationMessage.Builder builder = new ValidationMessage.Builder(); - builder.code(errorMessageType.getErrorCode()).path(at).schemaPath(schemaPath).details(details) - .format(messageFormat).type(type); - return builder.build(); - } - public static Builder builder() { return new Builder(); } - public static class Builder { - private String type; - private String code; - private String path; - private String schemaPath; - private Object[] arguments; - private Map details; - private MessageFormat format; - private String message; - private Supplier messageSupplier; - private MessageFormatter messageFormatter; - private String messageKey; - - public Builder type(String type) { - this.type = type; + public static class Builder extends BuilderSupport { + @Override + public Builder self() { return this; } + } + + public static abstract class BuilderSupport { + public abstract S self(); + + protected String type; + protected String code; + protected JsonNodePath evaluationPath; + protected SchemaLocation schemaLocation; + protected JsonNodePath instanceLocation; + protected String property; + protected Object[] arguments; + protected Map details; + protected MessageFormat format; + protected String message; + protected Supplier messageSupplier; + protected MessageFormatter messageFormatter; + protected String messageKey; + + public S type(String type) { + this.type = type; + return self(); + } - public Builder code(String code) { + public S code(String code) { this.code = code; - return this; + return self(); } - public Builder path(String path) { - this.path = path; - return this; + /** + * The instance location is the location of the JSON value within the root + * instance being validated. + * + * @param instanceLocation the instance location + * @return the builder + */ + public S instanceLocation(JsonNodePath instanceLocation) { + this.instanceLocation = instanceLocation; + return self(); } - public Builder schemaPath(String schemaPath) { - this.schemaPath = schemaPath; - return this; + /** + * 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. + * + * @param schemaLocation the schema location + * @return the builder + */ + public S schemaLocation(SchemaLocation schemaLocation) { + this.schemaLocation = schemaLocation; + return self(); } - public Builder arguments(Object... arguments) { + /** + * 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. + * + * @param evaluationPath the evaluation path + * @return the builder + */ + public S evaluationPath(JsonNodePath evaluationPath) { + this.evaluationPath = evaluationPath; + return self(); + } + + public S property(String property) { + this.property = property; + return self(); + } + + public S arguments(Object... arguments) { this.arguments = arguments; - return this; + return self(); } - public Builder details(Map details) { + public S details(Map details) { this.details = details; - return this; + return self(); } - public Builder format(MessageFormat format) { + public S format(MessageFormat format) { this.format = format; - return this; + return self(); } @Deprecated - public Builder customMessage(String message) { + public S customMessage(String message) { return message(message); } @@ -207,24 +269,24 @@ public Builder customMessage(String message) { * @param message the message pattern * @return the builder */ - public Builder message(String message) { + public S message(String message) { this.message = message; - return this; + return self(); } - public Builder messageSupplier(Supplier messageSupplier) { + public S messageSupplier(Supplier messageSupplier) { this.messageSupplier = messageSupplier; - return this; + return self(); } - public Builder messageFormatter(MessageFormatter messageFormatter) { + public S messageFormatter(MessageFormatter messageFormatter) { this.messageFormatter = messageFormatter; - return this; + return self(); } - public Builder messageKey(String messageKey) { + public S messageKey(String messageKey) { this.messageKey = messageKey; - return this; + return self(); } public ValidationMessage build() { @@ -234,23 +296,24 @@ public ValidationMessage build() { if (StringUtils.isNotBlank(this.message)) { messageKey = this.message; if (this.message.contains("{")) { - Object[] objs = getArguments(); + Object[] objs = getMessageArguments(); MessageFormat format = new MessageFormat(this.message); messageSupplier = new CachingSupplier<>(() -> format.format(objs)); } else { messageSupplier = message::toString; } } else if (messageSupplier == null) { - Object[] objs = getArguments(); + Object[] objs = getMessageArguments(); MessageFormatter formatter = this.messageFormatter != null ? this.messageFormatter : format::format; messageSupplier = new CachingSupplier<>(() -> formatter.format(objs)); } - return new ValidationMessage(type, code, path, schemaPath, arguments, details, messageKey, messageSupplier); + return new ValidationMessage(type, code, evaluationPath, schemaLocation, instanceLocation, + property, arguments, details, messageKey, messageSupplier); } - private Object[] getArguments() { + protected Object[] getMessageArguments() { Object[] objs = new Object[(arguments == null ? 0 : arguments.length) + 1]; - objs[0] = path; + objs[0] = instanceLocation; if (arguments != null) { for (int i = 1; i < objs.length; i++) { objs[i] = arguments[i - 1]; @@ -258,5 +321,57 @@ private Object[] getArguments() { } return objs; } + + protected String getType() { + return type; + } + + protected String getCode() { + return code; + } + + protected JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + protected SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + protected JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + protected String getProperty() { + return property; + } + + protected Object[] getArguments() { + return arguments; + } + + protected Map getDetails() { + return details; + } + + protected MessageFormat getFormat() { + return format; + } + + protected String getMessage() { + return message; + } + + protected Supplier getMessageSupplier() { + return messageSupplier; + } + + protected MessageFormatter getMessageFormatter() { + return messageFormatter; + } + + protected String getMessageKey() { + return messageKey; + } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 4341979ed..699b521bd 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -4,63 +4,47 @@ import com.networknt.schema.i18n.MessageSource; import com.networknt.schema.utils.StringUtils; -import java.util.Locale; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Objects; public abstract class ValidationMessageHandler { - protected final boolean failFast; - protected final Map customMessage; + protected boolean failFast; protected final MessageSource messageSource; - protected ValidatorTypeCode validatorType; protected ErrorMessageType errorMessageType; - protected String schemaPath; + protected SchemaLocation schemaLocation; + protected JsonNodePath evaluationPath; protected JsonSchema parentSchema; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, Map customMessage, MessageSource messageSource, ValidatorTypeCode validatorType, JsonSchema parentSchema, String schemaPath) { + protected boolean customErrorMessagesEnabled; + protected Map errorMessage; + + protected Keyword keyword; + + protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, + boolean customErrorMessagesEnabled, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, + SchemaLocation schemaLocation, JsonNodePath evaluationPath) { this.failFast = failFast; this.errorMessageType = errorMessageType; - this.customMessage = customMessage; this.messageSource = messageSource; - this.validatorType = validatorType; - this.schemaPath = schemaPath; + this.schemaLocation = Objects.requireNonNull(schemaLocation); + this.evaluationPath = Objects.requireNonNull(evaluationPath); this.parentSchema = parentSchema; + this.customErrorMessagesEnabled = customErrorMessagesEnabled; + updateKeyword(keyword); } - protected ValidationMessage buildValidationMessage(String propertyName, String at, Locale locale, Object... arguments) { - return buildValidationMessage(propertyName, at, getErrorMessageType().getErrorCodeValue(), locale, arguments); - } - - protected ValidationMessage buildValidationMessage(String propertyName, String at, String messageKey, Locale locale, Object... arguments) { - String messagePattern = null; - if (this.customMessage != null) { - messagePattern = this.customMessage.get(""); - if (propertyName != null) { - String specificMessagePattern = this.customMessage.get(propertyName); - if (specificMessagePattern != null) { - messagePattern = specificMessagePattern; - } + protected MessageSourceValidationMessage.Builder message() { + return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, message -> { + if (this.failFast && isApplicator()) { + throw new JsonSchemaException(message); } - } - final ValidationMessage message = ValidationMessage.builder() - .code(getErrorMessageType().getErrorCode()) - .path(at) - .schemaPath(this.schemaPath) - .arguments(arguments) - .messageKey(messageKey) - .messageFormatter(args -> this.messageSource.getMessage(messageKey, locale, args)) - .type(getValidatorType().getValue()) - .message(messagePattern) - .build(); - if (this.failFast && isApplicator()) { - throw new JsonSchemaException(message); - } - return message; - } - - protected ValidatorTypeCode getValidatorType() { - return this.validatorType; + }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) + .evaluationPath(this.evaluationPath).type(this.keyword != null ? this.keyword.getValue() : null) + .messageKey(getErrorMessageType().getErrorCodeValue()); } protected ErrorMessageType getErrorMessageType() { @@ -74,38 +58,114 @@ private boolean isApplicator() { && !isPartOfOneOfMultipleType(); } - private boolean isPartOfAnyOfMultipleType() { - return this.parentSchema.schemaPath.contains("/" + ValidatorTypeCode.ANY_OF.getValue() + "/"); + return schemaLocationContains(ValidatorTypeCode.ANY_OF.getValue()); } private boolean isPartOfIfMultipleType() { - return this.parentSchema.schemaPath.contains("/" + ValidatorTypeCode.IF_THEN_ELSE.getValue() + "/"); + return schemaLocationContains(ValidatorTypeCode.IF_THEN_ELSE.getValue()); } private boolean isPartOfNotMultipleType() { - return this.parentSchema.schemaPath.contains("/" + ValidatorTypeCode.NOT.getValue() + "/"); + return schemaLocationContains(ValidatorTypeCode.NOT.getValue()); + } + + protected boolean schemaLocationContains(String match) { + int count = this.parentSchema.schemaLocation.getFragment().getNameCount(); + for (int x = 0; x < count; x++) { + String name = this.parentSchema.schemaLocation.getFragment().getName(x); + if (match.equals(name)) { + return true; + } + } + return false; } /* ********************** START OF OpenAPI 3.0.x DISCRIMINATOR METHODS ********************************* */ protected boolean isPartOfOneOfMultipleType() { - return this.parentSchema.schemaPath.contains("/" + ValidatorTypeCode.ONE_OF.getValue() + "/"); + return schemaLocationContains(ValidatorTypeCode.ONE_OF.getValue()); } protected void parseErrorCode(String errorCodeKey) { - JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey); - if (errorCodeNode != null && errorCodeNode.isTextual()) { - String errorCodeText = errorCodeNode.asText(); - if (StringUtils.isNotBlank(errorCodeText)) { - this.errorMessageType = CustomErrorMessageType.of(errorCodeText); + if (errorCodeKey != null && this.parentSchema != null) { + JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey); + if (errorCodeNode != null && errorCodeNode.isTextual()) { + String errorCodeText = errorCodeNode.asText(); + if (StringUtils.isNotBlank(errorCodeText)) { + this.errorMessageType = CustomErrorMessageType.of(errorCodeText); + } } } } protected void updateValidatorType(ValidatorTypeCode validatorTypeCode) { - this.validatorType = validatorTypeCode; - this.errorMessageType = validatorTypeCode; - parseErrorCode(validatorTypeCode.getErrorCodeKey()); + updateKeyword(validatorTypeCode); + updateErrorMessageType(validatorTypeCode); + } + + protected void updateErrorMessageType(ErrorMessageType errorMessageType) { + this.errorMessageType = errorMessageType; + } + + protected void updateKeyword(Keyword keyword) { + this.keyword = keyword; + if (this.keyword != null) { + if (this.customErrorMessagesEnabled && keyword != null && parentSchema != null) { + this.errorMessage = getErrorMessage(parentSchema.getSchemaNode(), keyword.getValue()); + } + parseErrorCode(getErrorCodeKey(keyword.getValue())); + } + } + + /** + * Gets the custom error message to use. + * + * @param schemaNode the schema node + * @param keyword the keyword + * @return the custom error message + */ + protected Map getErrorMessage(JsonNode schemaNode, String keyword) { + final JsonSchema parentSchema = this.parentSchema; + final JsonNode message = getMessageNode(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(JsonNode schemaNode, JsonSchema parentSchema, String pname) { + if (schemaNode.get("message") != null && schemaNode.get("message").get(pname) != null) { + return schemaNode.get("message"); + } + JsonNode messageNode; + messageNode = schemaNode.get("message"); + if (messageNode == null && parentSchema != null) { + messageNode = parentSchema.schemaNode.get("message"); + if (messageNode == null) { + return getMessageNode(parentSchema.schemaNode, parentSchema.getParentSchema(), pname); + } + } + return messageNode; + } + + protected String getErrorCodeKey(String keyword) { + if (keyword != null) { + return keyword + "ErrorCode"; + } + return null; } } diff --git a/src/main/java/com/networknt/schema/ValidatorState.java b/src/main/java/com/networknt/schema/ValidatorState.java index e9620b83e..b522bd826 100644 --- a/src/main/java/com/networknt/schema/ValidatorState.java +++ b/src/main/java/com/networknt/schema/ValidatorState.java @@ -16,9 +16,6 @@ package com.networknt.schema; public class ValidatorState { - - public static final String VALIDATOR_STATE_KEY = "com.networknt.schema.ValidatorState"; - /** * Flag set when a node has matched Works in conjunction with the next flag: * isComplexValidator, to be used for complex validators such as oneOf, for ex @@ -42,6 +39,23 @@ public class ValidatorState { */ private boolean isValidationEnabled = false; + /** + * Constructor for validation state. + */ + public ValidatorState() { + } + + /** + * Constructor for validation state. + * + * @param walkEnabled whether walk is enabled + * @param validationEnabled whether validation is enabled + */ + public ValidatorState(boolean walkEnabled, boolean validationEnabled) { + this.isWalkEnabled = walkEnabled; + this.isValidationEnabled = validationEnabled; + } + public void setMatchedNode(boolean matchedNode) { this.matchedNode = matchedNode; } diff --git a/src/main/java/com/networknt/schema/ValidatorTypeCode.java b/src/main/java/com/networknt/schema/ValidatorTypeCode.java index f87006f67..786b8ed8c 100644 --- a/src/main/java/com/networknt/schema/ValidatorTypeCode.java +++ b/src/main/java/com/networknt/schema/ValidatorTypeCode.java @@ -19,13 +19,18 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.SpecVersion.VersionFlag; -import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; +@FunctionalInterface +interface ValidatorFactory { + JsonValidator newInstance(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext); +} + enum VersionCode { AllVersions(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V4, SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }), MinV6(new SpecVersion.VersionFlag[] { SpecVersion.VersionFlag.V6, SpecVersion.VersionFlag.V7, SpecVersion.VersionFlag.V201909, SpecVersion.VersionFlag.V202012 }), @@ -50,61 +55,61 @@ EnumSet getVersions() { } public enum ValidatorTypeCode implements Keyword, ErrorMessageType { - ADDITIONAL_PROPERTIES("additionalProperties", "1001", AdditionalPropertiesValidator.class, VersionCode.AllVersions), - ALL_OF("allOf", "1002", AllOfValidator.class, VersionCode.AllVersions), - ANY_OF("anyOf", "1003", AnyOfValidator.class, VersionCode.AllVersions), - CONST("const", "1042", ConstValidator.class, VersionCode.MinV6), - CONTAINS("contains", "1043", ContainsValidator.class, VersionCode.MinV6), + ADDITIONAL_PROPERTIES("additionalProperties", "1001", AdditionalPropertiesValidator::new, VersionCode.AllVersions), + ALL_OF("allOf", "1002", AllOfValidator::new, VersionCode.AllVersions), + ANY_OF("anyOf", "1003", AnyOfValidator::new, VersionCode.AllVersions), + CONST("const", "1042", ConstValidator::new, VersionCode.MinV6), + CONTAINS("contains", "1043", ContainsValidator::new, VersionCode.MinV6), CROSS_EDITS("crossEdits", "1004", null, VersionCode.AllVersions), DATETIME("dateTime", "1034", null, VersionCode.AllVersions), - DEPENDENCIES("dependencies", "1007", DependenciesValidator.class, VersionCode.AllVersions), - DEPENDENT_REQUIRED("dependentRequired", "1045", DependentRequired.class, VersionCode.MinV201909), - DEPENDENT_SCHEMAS("dependentSchemas", "1046", DependentSchemas.class, VersionCode.MinV201909), + DEPENDENCIES("dependencies", "1007", DependenciesValidator::new, VersionCode.AllVersions), + DEPENDENT_REQUIRED("dependentRequired", "1045", DependentRequired::new, VersionCode.MinV201909), + DEPENDENT_SCHEMAS("dependentSchemas", "1046", DependentSchemas::new, VersionCode.MinV201909), EDITS("edits", "1005", null, VersionCode.AllVersions), - ENUM("enum", "1008", EnumValidator.class, VersionCode.AllVersions), - EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", ExclusiveMaximumValidator.class, VersionCode.MinV6), - EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", ExclusiveMinimumValidator.class, VersionCode.MinV6), - FALSE("false", "1041", FalseValidator.class, VersionCode.MinV6), + ENUM("enum", "1008", EnumValidator::new, VersionCode.AllVersions), + EXCLUSIVE_MAXIMUM("exclusiveMaximum", "1038", ExclusiveMaximumValidator::new, VersionCode.MinV6), + EXCLUSIVE_MINIMUM("exclusiveMinimum", "1039", ExclusiveMinimumValidator::new, VersionCode.MinV6), + FALSE("false", "1041", FalseValidator::new, VersionCode.MinV6), FORMAT("format", "1009", null, VersionCode.AllVersions) { - @Override public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + @Override public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { throw new UnsupportedOperationException("Use FormatKeyword instead"); } }, ID("id", "1036", null, VersionCode.AllVersions), - IF_THEN_ELSE("if", "1037", IfValidator.class, VersionCode.MinV7), - ITEMS_202012("items", "1010", ItemsValidator202012.class, VersionCode.MinV202012), - ITEMS("items", "1010", ItemsValidator.class, VersionCode.MaxV201909), - MAX_CONTAINS("maxContains", "1006", MinMaxContainsValidator.class, VersionCode.MinV201909), - MAX_ITEMS("maxItems", "1012", MaxItemsValidator.class, VersionCode.AllVersions), - MAX_LENGTH("maxLength", "1013", MaxLengthValidator.class, VersionCode.AllVersions), - MAX_PROPERTIES("maxProperties", "1014", MaxPropertiesValidator.class, VersionCode.AllVersions), - MAXIMUM("maximum", "1011", MaximumValidator.class, VersionCode.AllVersions), - MIN_CONTAINS("minContains", "1049", MinMaxContainsValidator.class, VersionCode.MinV201909), - MIN_ITEMS("minItems", "1016", MinItemsValidator.class, VersionCode.AllVersions), - MIN_LENGTH("minLength", "1017", MinLengthValidator.class, VersionCode.AllVersions), - MIN_PROPERTIES("minProperties", "1018", MinPropertiesValidator.class, VersionCode.AllVersions), - MINIMUM("minimum", "1015", MinimumValidator.class, VersionCode.AllVersions), - MULTIPLE_OF("multipleOf", "1019", MultipleOfValidator.class, VersionCode.AllVersions), - NOT_ALLOWED("notAllowed", "1033", NotAllowedValidator.class, VersionCode.AllVersions), - NOT("not", "1020", NotValidator.class, VersionCode.AllVersions), - ONE_OF("oneOf", "1022", OneOfValidator.class, VersionCode.AllVersions), - PATTERN_PROPERTIES("patternProperties", "1024", PatternPropertiesValidator.class, VersionCode.AllVersions), - PATTERN("pattern", "1023", PatternValidator.class, VersionCode.AllVersions), - PREFIX_ITEMS("prefixItems", "1048", PrefixItemsValidator.class, VersionCode.MinV202012), - PROPERTIES("properties", "1025", PropertiesValidator.class, VersionCode.AllVersions), - PROPERTYNAMES("propertyNames", "1044", PropertyNamesValidator.class, VersionCode.MinV6), - READ_ONLY("readOnly", "1032", ReadOnlyValidator.class, VersionCode.MinV7), - RECURSIVE_REF("$recursiveRef", "1050", RecursiveRefValidator.class, VersionCode.V201909), - REF("$ref", "1026", RefValidator.class, VersionCode.AllVersions), - REQUIRED("required", "1028", RequiredValidator.class, VersionCode.AllVersions), - TRUE("true", "1040", TrueValidator.class, VersionCode.MinV6), - TYPE("type", "1029", TypeValidator.class, VersionCode.AllVersions), - UNEVALUATED_ITEMS("unevaluatedItems", "1021", UnevaluatedItemsValidator.class, VersionCode.MinV201909), - UNEVALUATED_PROPERTIES("unevaluatedProperties","1047",UnevaluatedPropertiesValidator.class,VersionCode.MinV6), - UNION_TYPE("unionType", "1030", UnionTypeValidator.class, VersionCode.AllVersions), - UNIQUE_ITEMS("uniqueItems", "1031", UniqueItemsValidator.class, VersionCode.AllVersions), + IF_THEN_ELSE("if", "1037", IfValidator::new, VersionCode.MinV7), + ITEMS_202012("items", "1010", ItemsValidator202012::new, VersionCode.MinV202012), + ITEMS("items", "1010", ItemsValidator::new, VersionCode.MaxV201909), + MAX_CONTAINS("maxContains", "1006", MinMaxContainsValidator::new, VersionCode.MinV201909), + MAX_ITEMS("maxItems", "1012", MaxItemsValidator::new, VersionCode.AllVersions), + MAX_LENGTH("maxLength", "1013", MaxLengthValidator::new, VersionCode.AllVersions), + MAX_PROPERTIES("maxProperties", "1014", MaxPropertiesValidator::new, VersionCode.AllVersions), + MAXIMUM("maximum", "1011", MaximumValidator::new, VersionCode.AllVersions), + MIN_CONTAINS("minContains", "1049", MinMaxContainsValidator::new, VersionCode.MinV201909), + MIN_ITEMS("minItems", "1016", MinItemsValidator::new, VersionCode.AllVersions), + MIN_LENGTH("minLength", "1017", MinLengthValidator::new, VersionCode.AllVersions), + MIN_PROPERTIES("minProperties", "1018", MinPropertiesValidator::new, VersionCode.AllVersions), + MINIMUM("minimum", "1015", MinimumValidator::new, VersionCode.AllVersions), + MULTIPLE_OF("multipleOf", "1019", MultipleOfValidator::new, VersionCode.AllVersions), + NOT_ALLOWED("notAllowed", "1033", NotAllowedValidator::new, VersionCode.AllVersions), + NOT("not", "1020", NotValidator::new, VersionCode.AllVersions), + ONE_OF("oneOf", "1022", OneOfValidator::new, VersionCode.AllVersions), + PATTERN_PROPERTIES("patternProperties", "1024", PatternPropertiesValidator::new, VersionCode.AllVersions), + PATTERN("pattern", "1023", PatternValidator::new, VersionCode.AllVersions), + PREFIX_ITEMS("prefixItems", "1048", PrefixItemsValidator::new, VersionCode.MinV202012), + PROPERTIES("properties", "1025", PropertiesValidator::new, VersionCode.AllVersions), + PROPERTYNAMES("propertyNames", "1044", PropertyNamesValidator::new, VersionCode.MinV6), + READ_ONLY("readOnly", "1032", ReadOnlyValidator::new, VersionCode.MinV7), + RECURSIVE_REF("$recursiveRef", "1050", RecursiveRefValidator::new, VersionCode.V201909), + REF("$ref", "1026", RefValidator::new, VersionCode.AllVersions), + REQUIRED("required", "1028", RequiredValidator::new, VersionCode.AllVersions), + TRUE("true", "1040", TrueValidator::new, VersionCode.MinV6), + TYPE("type", "1029", TypeValidator::new, VersionCode.AllVersions), + UNEVALUATED_ITEMS("unevaluatedItems", "1021", UnevaluatedItemsValidator::new, VersionCode.MinV201909), + UNEVALUATED_PROPERTIES("unevaluatedProperties","1047",UnevaluatedPropertiesValidator::new,VersionCode.MinV201909), + UNION_TYPE("unionType", "1030", UnionTypeValidator::new, VersionCode.AllVersions), + UNIQUE_ITEMS("uniqueItems", "1031", UniqueItemsValidator::new, VersionCode.AllVersions), UUID("uuid", "1035", null, VersionCode.AllVersions), - WRITE_ONLY("writeOnly", "1027", WriteOnlyValidator.class, VersionCode.MinV7), + WRITE_ONLY("writeOnly", "1027", WriteOnlyValidator::new, VersionCode.MinV7), ; private static final Map CONSTANTS = new HashMap<>(); @@ -117,19 +122,14 @@ public enum ValidatorTypeCode implements Keyword, ErrorMessageType { private final String value; private final String errorCode; - private Map customMessage; - private final String errorCodeKey; - private final Class validator; + private final ValidatorFactory validatorFactory; private final VersionCode versionCode; - - private ValidatorTypeCode(String value, String errorCode, Class validator, VersionCode versionCode) { + private ValidatorTypeCode(String value, String errorCode, ValidatorFactory validatorFactory, VersionCode versionCode) { this.value = value; this.errorCode = errorCode; - this.errorCodeKey = value + "ErrorCode"; - this.validator = validator; + this.validatorFactory = validatorFactory; this.versionCode = versionCode; - this.customMessage = null; } public static List getNonFormatKeywords(SpecVersion.VersionFlag versionFlag) { @@ -151,15 +151,13 @@ public static ValidatorTypeCode fromValue(String value) { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws Exception { - if (this.validator == null) { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { + if (this.validatorFactory == null) { throw new UnsupportedOperationException("No suitable validator for " + getValue()); } - // if the config version is not match the validator - @SuppressWarnings("unchecked") - Constructor c = ((Class) this.validator).getConstructor( - new Class[]{String.class, JsonNode.class, JsonSchema.class, ValidationContext.class}); - return c.newInstance(schemaPath + "/" + getValue(), schemaNode, parentSchema, validationContext); + return validatorFactory.newInstance(schemaLocation, evaluationPath, schemaNode, parentSchema, + validationContext); } @Override @@ -177,20 +175,6 @@ public String getErrorCode() { return this.errorCode; } - @Override - public void setCustomMessage(Map message) { - this.customMessage = message; - } - - @Override - public Map getCustomMessage() { - return this.customMessage; - } - - public String getErrorCodeKey() { - return this.errorCodeKey; - } - public VersionCode getVersionCode() { return this.versionCode; } diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index 3047a6ea3..71171920c 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -13,19 +13,19 @@ public class WriteOnlyValidator extends BaseJsonValidator { private final boolean writeOnly; - public WriteOnlyValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.WRITE_ONLY, validationContext); + public WriteOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.WRITE_ONLY, validationContext); this.writeOnly = validationContext.getConfig().isWriteOnly(); logger.debug("Loaded WriteOnlyValidator for property {} as {}", parentSchema, "write mode"); - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); if (this.writeOnly) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale())); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 0117d58e1..5ac8352c9 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -21,8 +21,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.BaseJsonValidator; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonType; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.TypeFactory; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -38,24 +40,23 @@ public class DateTimeValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class); private static final String DATETIME = "date-time"; - public DateTimeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, ValidatorTypeCode type) { - super(schemaPath, schemaNode, parentSchema, type, validationContext); - + public DateTimeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, ValidatorTypeCode type) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, type, validationContext); this.validationContext = validationContext; - parseErrorCode(getValidatorType().getErrorCodeKey()); } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - debug(logger, node, rootNode, at); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + debug(logger, node, rootNode, instanceLocation); JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { return Collections.emptySet(); } if (!isLegalDateTime(node.textValue())) { - return Collections.singleton(buildValidationMessage(null, at, executionContext.getExecutionConfig().getLocale(), - node.textValue(), DATETIME)); + return Collections.singleton(message().instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java index 761c8e429..57ea0f21d 100644 --- a/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -12,11 +14,13 @@ public abstract class AbstractWalkListenerRunner implements WalkListenerRunner { - protected WalkEvent constructWalkEvent(ExecutionContext executionContext, String keyWordName, JsonNode node, JsonNode rootNode, - String at, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { - return WalkEvent.builder().executionContext(executionContext).at(at).keyWordName(keyWordName).node(node) - .parentSchema(parentSchema).rootNode(rootNode).schemaNode(schemaNode).schemaPath(schemaPath) + protected WalkEvent constructWalkEvent(ExecutionContext executionContext, String keyword, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, + JsonSchemaFactory currentJsonSchemaFactory) { + return WalkEvent.builder().executionContext(executionContext).instanceLocation(instanceLocation) + .evaluationPath(evaluationPath).keyword(keyword).node(node).parentSchema(parentSchema) + .rootNode(rootNode).schemaNode(schemaNode).schemaLocation(schemaLocation) .currentJsonSchemaFactory(currentJsonSchemaFactory).validationContext(validationContext).build(); } diff --git a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java index eb4360a70..429771c04 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -19,20 +21,22 @@ public DefaultItemWalkListenerRunner(List itemWalkListen } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, - String at, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyWordPath, node, rootNode, at, schemaPath, schemaNode, parentSchema, - validationContext, currentJsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, + JsonSchemaFactory currentJsonSchemaFactory) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, + evaluationPath, schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); return runPreWalkListeners(itemWalkListeners, walkEvent); } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, String at, - String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyWordPath, node, rootNode, at, schemaPath, schemaNode, - parentSchema, validationContext, currentJsonSchemaFactory); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, + JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, + evaluationPath, schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); runPostWalkListeners(itemWalkListeners, walkEvent, validationMessages); } diff --git a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java index 7aaa729e3..04b1106a5 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java @@ -15,19 +15,14 @@ public DefaultKeywordWalkListenerRunner(Map this.keywordWalkListenersMap = keywordWalkListenersMap; } - protected String getKeywordName(String keyWordPath) { - return keyWordPath.substring(keyWordPath.lastIndexOf('/') + 1); - } - @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, - String at, String schemaPath, JsonNode schemaNode, - JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { - String keyword = getKeywordName(keyWordPath); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { boolean continueRunningListenersAndWalk = true; - WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, at, schemaPath, - schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, + schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); // Run Listeners that are setup only for this keyword. List currentKeywordListeners = keywordWalkListenersMap.get(keyword); continueRunningListenersAndWalk = runPreWalkListeners(currentKeywordListeners, keywordWalkEvent); @@ -41,14 +36,13 @@ public boolean runPreWalkListeners(ExecutionContext executionContext, String key } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, String at, - String schemaPath, JsonNode schemaNode, + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, + JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { - String keyword = getKeywordName(keyWordPath); - WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, at, schemaPath, - schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { + WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, + schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); // Run Listeners that are setup only for this keyword. List currentKeywordListeners = keywordWalkListenersMap.get(keyword); runPostWalkListeners(currentKeywordListeners, keywordWalkEvent, validationMessages); diff --git a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java index 0ea6a7c5d..a8e937d22 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -19,20 +21,20 @@ public DefaultPropertyWalkListenerRunner(List propertyWa } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, - String at, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyWordPath, node, rootNode, at, schemaPath, schemaNode, - parentSchema, validationContext, currentJsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, schemaLocation, + schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); return runPreWalkListeners(propertyWalkListeners, walkEvent); } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, String at, - String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyWordPath, node, rootNode, at, schemaPath, schemaNode, - parentSchema, validationContext, currentJsonSchemaFactory); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, + JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNode schemaNode, JsonSchema parentSchema, + ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory, Set validationMessages) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, schemaLocation, + schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); runPostWalkListeners(propertyWalkListeners, walkEvent, validationMessages); } diff --git a/src/main/java/com/networknt/schema/walk/JsonSchemaWalker.java b/src/main/java/com/networknt/schema/walk/JsonSchemaWalker.java index 5d6d9d043..1a2b4355f 100644 --- a/src/main/java/com/networknt/schema/walk/JsonSchemaWalker.java +++ b/src/main/java/com/networknt/schema/walk/JsonSchemaWalker.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.BaseJsonValidator; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.ValidationMessage; import java.util.Set; @@ -15,7 +16,7 @@ public interface JsonSchemaWalker { * cutting concerns like logging or instrumentation. This method also performs * the validation if {@code shouldValidateSchema} is set to true.
*
- * {@link BaseJsonValidator#walk(ExecutionContext, JsonNode, JsonNode, String, boolean)} provides + * {@link BaseJsonValidator#walk(ExecutionContext, JsonNode, JsonNode, JsonNodePath, boolean)} provides * a default implementation of this method. However validators that parse * sub-schemas should override this method to call walk method on those * sub-schemas. @@ -23,9 +24,10 @@ public interface JsonSchemaWalker { * @param executionContext ExecutionContext * @param node JsonNode * @param rootNode JsonNode - * @param at String + * @param instanceLocation JsonNodePath * @param shouldValidateSchema boolean * @return a set of validation messages if shouldValidateSchema is true. */ - Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema); + Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema); } diff --git a/src/main/java/com/networknt/schema/walk/WalkEvent.java b/src/main/java/com/networknt/schema/walk/WalkEvent.java index 83c020b31..9569e9e6a 100644 --- a/src/main/java/com/networknt/schema/walk/WalkEvent.java +++ b/src/main/java/com/networknt/schema/walk/WalkEvent.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.SchemaValidatorsConfig; import com.networknt.schema.ValidationContext; @@ -15,13 +17,14 @@ public class WalkEvent { private ExecutionContext executionContext; - private String schemaPath; + private SchemaLocation schemaLocation; + private JsonNodePath evaluationPath; private JsonNode schemaNode; private JsonSchema parentSchema; - private String keyWordName; + private String keyword; private JsonNode node; private JsonNode rootNode; - private String at; + private JsonNodePath instanceLocation; private JsonSchemaFactory currentJsonSchemaFactory; private ValidationContext validationContext; @@ -29,8 +32,12 @@ public ExecutionContext getExecutionContext() { return executionContext; } - public String getSchemaPath() { - return schemaPath; + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + public JsonNodePath getEvaluationPath() { + return evaluationPath; } public JsonNode getSchemaNode() { @@ -41,8 +48,8 @@ public JsonSchema getParentSchema() { return parentSchema; } - public String getKeyWordName() { - return keyWordName; + public String getKeyword() { + return keyword; } public JsonNode getNode() { @@ -53,8 +60,8 @@ public JsonNode getRootNode() { return rootNode; } - public String getAt() { - return at; + public JsonNodePath getInstanceLocation() { + return instanceLocation; } public JsonSchema getRefSchema(URI schemaUri) { @@ -73,6 +80,12 @@ public JsonSchemaFactory getCurrentJsonSchemaFactory() { return currentJsonSchemaFactory; } + @Override + public String toString() { + return "WalkEvent [evaluationPath=" + evaluationPath + ", schemaLocation=" + schemaLocation + + ", instanceLocation=" + instanceLocation + "]"; + } + static class WalkEventBuilder { private WalkEvent walkEvent; @@ -86,8 +99,13 @@ public WalkEventBuilder executionContext(ExecutionContext executionContext) { return this; } - public WalkEventBuilder schemaPath(String schemaPath) { - walkEvent.schemaPath = schemaPath; + public WalkEventBuilder evaluationPath(JsonNodePath evaluationPath) { + walkEvent.evaluationPath = evaluationPath; + return this; + } + + public WalkEventBuilder schemaLocation(SchemaLocation schemaLocation) { + walkEvent.schemaLocation = schemaLocation; return this; } @@ -101,8 +119,8 @@ public WalkEventBuilder parentSchema(JsonSchema parentSchema) { return this; } - public WalkEventBuilder keyWordName(String keyWordName) { - walkEvent.keyWordName = keyWordName; + public WalkEventBuilder keyword(String keyword) { + walkEvent.keyword = keyword; return this; } @@ -116,8 +134,8 @@ public WalkEventBuilder rootNode(JsonNode rootNode) { return this; } - public WalkEventBuilder at(String at) { - walkEvent.at = at; + public WalkEventBuilder instanceLocation(JsonNodePath instanceLocation) { + walkEvent.instanceLocation = instanceLocation; return this; } diff --git a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java index b0d45be86..4bba2c63c 100644 --- a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -11,11 +13,14 @@ public interface WalkListenerRunner { - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, - String at, String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, JsonSchemaFactory jsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, + JsonSchemaFactory jsonSchemaFactory); - public void runPostWalkListeners(ExecutionContext executionContext, String keyWordPath, JsonNode node, JsonNode rootNode, String at, - String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory jsonSchemaFactory, Set validationMessages); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, + JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, + JsonSchemaFactory jsonSchemaFactory, Set validationMessages); } diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 623e19f73..bceacc396 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -1,5 +1,5 @@ $ref = {0}: has an error with ''refs'' -additionalProperties = {0}.{1}: is not defined in the schema and the schema does not allow additional properties +additionalProperties = {0}: is not defined in the schema and the schema does not allow additional properties allOf = {0}: should be valid to all the schemas {1} anyOf = {0}: should be valid to any of the schemas {1} const = {0}: must be a constant value {1} @@ -18,7 +18,7 @@ exclusiveMinimum = {0}: must have an exclusive minimum value of {1} false = Boolean schema false is not valid format = {0}: does not match the {1} pattern {2} id = {0}: {1} is an invalid segment for URI {2} -items = {0}[{1}]: no validator found at this index +items = {0}: no validator found at this index maxContains = {0}: must be a non-negative integer in {1} maxItems = {0}: there must be a maximum of {1} items in the array maxLength = {0}: may only be {1} characters long @@ -26,24 +26,24 @@ maxProperties = {0}: may only have a maximum of {1} properties maximum = {0}: must have a maximum value of {1} minContains = {0}: must be a non-negative integer in {1} minContainsVsMaxContains = {0}: minContains must less than or equal to maxContains in {1} -minItems = {0}: there must be a minimum of {1} items in the array +minItems = {0}: expected at least {1} items but found {2} minLength = {0}: must be at least {1} characters long minProperties = {0}: should have a minimum of {1} properties minimum = {0}: must have a minimum value of {1} multipleOf = {0}: must be multiple of {1} not = {0}: should not be valid to the schema {1} -notAllowed = {0}.{1}: is not allowed but it is in the data +notAllowed = {0}: is not allowed but it is in the data oneOf = {0}: should be valid to one and only one schema, but {1} are valid pattern = {0}: does not match the regex pattern {1} patternProperties = {0}: has some error with ''pattern properties'' -prefixItems = {0}[{1}]: no validator found at this index +prefixItems = {0}: no validator found at this index properties = {0}: has an error with ''properties'' propertyNames = Property name {0} is not valid for validation: {1} readOnly = {0}: is a readonly field, it cannot be changed -required = {0}.{1}: is missing but it is required +required = {0}: required property ''{1}'' not found type = {0}: {1} found, {2} expected -unevaluatedItems = There are unevaluated items at the following paths {0} -unevaluatedProperties = There are unevaluated properties at the following paths {0} +unevaluatedItems = {0}: must not have unevaluated items +unevaluatedProperties = {0}: must not have unevaluated properties unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique uuid = {0}: {1} is an invalid {2} diff --git a/src/test/java/com/networknt/schema/AbsoluteIriTest.java b/src/test/java/com/networknt/schema/AbsoluteIriTest.java new file mode 100644 index 000000000..c504b887f --- /dev/null +++ b/src/test/java/com/networknt/schema/AbsoluteIriTest.java @@ -0,0 +1,116 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class AbsoluteIriTest { + + @Test + void absolute() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/bar.json"); + assertEquals("classpath:resource", iri.resolve("classpath:resource").toString()); + } + + @Test + void resolveNull() { + AbsoluteIri iri = new AbsoluteIri(null); + assertEquals("test.json", iri.resolve("test.json").toString()); + } + + @Test + void relativeAtDocument() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/bar.json"); + assertEquals("http://www.example.org/foo/test.json", iri.resolve("test.json").toString()); + } + + @Test + void relativeAtDirectory() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/"); + assertEquals("http://www.example.org/foo/test.json", iri.resolve("test.json").toString()); + } + + @Test + void relativeAtRoot() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org"); + assertEquals("http://www.example.org/test.json", iri.resolve("test.json").toString()); + } + + @Test + void relativeAtRootWithTrailingSlash() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/"); + assertEquals("http://www.example.org/test.json", iri.resolve("test.json").toString()); + } + + @Test + void relativeAtRootWithSchemeSpecificPart() { + AbsoluteIri iri = new AbsoluteIri("classpath:resource"); + assertEquals("classpath:resource/test.json", iri.resolve("test.json").toString()); + } + + @Test + void rootAbsoluteAtDocument() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/bar.json"); + assertEquals("http://www.example.org/test.json", iri.resolve("/test.json").toString()); + } + + @Test + void rootAbsoluteAtDirectory() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/"); + assertEquals("http://www.example.org/test.json", iri.resolve("/test.json").toString()); + } + + @Test + void rootAbsoluteAtRoot() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org"); + assertEquals("http://www.example.org/test.json", iri.resolve("/test.json").toString()); + } + + @Test + void rootAbsoluteAtRootWithTrailingSlash() { + AbsoluteIri iri = new AbsoluteIri("http://www.example.org/"); + assertEquals("http://www.example.org/test.json", iri.resolve("/test.json").toString()); + } + + @Test + void rootAbsoluteAtRootSchemeSpecificPart() { + AbsoluteIri iri = new AbsoluteIri("classpath:resource"); + assertEquals("classpath:resource/test.json", iri.resolve("/test.json").toString()); + } + + @Test + void schemeClasspath() { + assertEquals("classpath", AbsoluteIri.of("classpath:resource/test.json").getScheme()); + } + + @Test + void schemeHttps() { + assertEquals("https", AbsoluteIri.of("https://www.example.org").getScheme()); + } + + @Test + void schemeNone() { + assertEquals("", AbsoluteIri.of("relative").getScheme()); + } + + @Test + void schemeUrn() { + assertEquals("urn", AbsoluteIri.of("urn:isbn:1234567890").getScheme()); + } + +} diff --git a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java index 71379c396..44d5bd1ee 100644 --- a/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/AbstractJsonSchemaTest.java @@ -21,8 +21,8 @@ public abstract class AbstractJsonSchemaTest { private static final String SCHEMA = "$schema"; private static final SpecVersion.VersionFlag DEFAULT_VERSION_FLAG = SpecVersion.VersionFlag.V202012; - private static final String ASSERT_MSG_ERROR_CODE = "Validation result should contain {} error code"; - private static final String ASSERT_MSG_TYPE = "Validation result should contain {} type"; + private static final String ASSERT_MSG_ERROR_CODE = "Validation result should contain {0} error code"; + private static final String ASSERT_MSG_TYPE = "Validation result should contain {0} type"; protected Set validate(String dataPath) { JsonNode dataNode = getJsonNodeFromPath(dataPath); diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java index 164f5b22b..779c27273 100644 --- a/src/test/java/com/networknt/schema/CollectorContextTest.java +++ b/src/test/java/com/networknt/schema/CollectorContextTest.java @@ -252,10 +252,10 @@ public String getValue() { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext) throws JsonSchemaException, Exception { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(); + return new CustomValidator(schemaLocation, evaluationPath); } return null; } @@ -267,10 +267,13 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc * This will be helpful in cases where we don't want to revisit the entire JSON * document again just for gathering this kind of information. */ - private class CustomValidator implements JsonValidator { + private class CustomValidator extends AbstractJsonValidator { + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { + super(schemaLocation, evaluationPath,null); + } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { // Get an instance of collector context. CollectorContext collectorContext = executionContext.getCollectorContext(); if (collectorContext.get(SAMPLE_COLLECTOR) == null) { @@ -281,12 +284,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set validate(ExecutionContext executionContext, JsonNode rootNode) { - return validate(executionContext, rootNode, rootNode, PathType.DEFAULT.getRoot()); - } - - @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { // Ignore this method for testing. return null; } @@ -325,10 +323,10 @@ public String getValue() { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext) throws JsonSchemaException, Exception { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator1(); + return new CustomValidator1(schemaLocation, evaluationPath); } return null; } @@ -342,10 +340,14 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc * we expect this validator to be called multiple times as the associated * keyword has been used multiple times in JSON Schema. */ - private class CustomValidator1 implements JsonValidator { + private class CustomValidator1 extends AbstractJsonValidator { + public CustomValidator1(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { + super(schemaLocation, evaluationPath,null); + } + @SuppressWarnings("unchecked") @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { // Get an instance of collector context. CollectorContext collectorContext = executionContext.getCollectorContext(); // If collector type is not added to context add one. @@ -358,12 +360,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } @Override - public Set validate(ExecutionContext executionContext, JsonNode rootNode) { - return validate(executionContext, rootNode, rootNode, PathType.DEFAULT.getRoot()); - } - - @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { // Ignore this method for testing. return null; } diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index e8411daa1..6ef1f0ab6 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -49,8 +49,9 @@ private static final class Validator extends AbstractJsonValidator { private final List enumNames; private final String keyword; - private Validator(String keyword, List enumValues, List enumNames) { - super(); + private Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword, + List enumValues, List enumNames) { + super(schemaLocation, evaluationPath,null); if (enumNames.size() != enumValues.size()) { throw new IllegalArgumentException("enum and enumNames need to be of same length"); } @@ -60,7 +61,7 @@ private Validator(String keyword, List enumValues, List enumName } @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { String value = node.asText(); int idx = enumValues.indexOf(value); if (idx < 0) { @@ -69,7 +70,7 @@ public Set validate(ExecutionContext executionContext, JsonNo String valueName = enumNames.get(idx); Set messages = new HashSet<>(); ValidationMessage validationMessage = ValidationMessage.builder().type(keyword) - .code("tests.example.enumNames").message("{0}: enumName is {1}").path(at) + .code("tests.example.enumNames").message("{0}: enumName is {1}").instanceLocation(instanceLocation) .arguments(valueName).build(); messages.add(validationMessage); return messages; @@ -82,8 +83,8 @@ public EnumNamesKeyword() { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext) throws JsonSchemaException, Exception { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { /* * You can access the schema node here to read data from your keyword */ @@ -96,7 +97,7 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc } JsonNode enumSchemaNode = parentSchemaNode.get("enum"); - return new Validator(getValue(), readStringList(enumSchemaNode), readStringList(schemaNode)); + return new Validator(schemaLocation, evaluationPath, getValue(), readStringList(enumSchemaNode), readStringList(schemaNode)); } private List readStringList(JsonNode node) { diff --git a/src/test/java/com/networknt/schema/Issue342Test.java b/src/test/java/com/networknt/schema/Issue342Test.java index 9a921cfb8..13f5b08bb 100644 --- a/src/test/java/com/networknt/schema/Issue342Test.java +++ b/src/test/java/com/networknt/schema/Issue342Test.java @@ -32,7 +32,7 @@ public void propertyNameEnumShouldFailV7() throws Exception { Set errors = schema.validate(node); Assertions.assertEquals(1, errors.size()); final ValidationMessage error = errors.iterator().next(); - Assertions.assertEquals("$.z", error.getPath()); + Assertions.assertEquals("$.z", error.getInstanceLocation().toString()); Assertions.assertEquals("Property name $.z is not valid for validation: does not have a value in the enumeration [a, b, c]", error.getMessage()); } } diff --git a/src/test/java/com/networknt/schema/Issue347Test.java b/src/test/java/com/networknt/schema/Issue347Test.java index 52fef1ae4..331ba9fb3 100644 --- a/src/test/java/com/networknt/schema/Issue347Test.java +++ b/src/test/java/com/networknt/schema/Issue347Test.java @@ -1,22 +1,21 @@ package com.networknt.schema; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; public class Issue347Test { @Test public void failure() { - ObjectMapper mapper = new ObjectMapper(); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); try { JsonSchema schema = factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json")); } catch (Throwable e) { assertThat(e, instanceOf(JsonSchemaException.class)); - assertEquals("test: null is an invalid segment for URI {2}", e.getMessage()); + assertEquals("/$id: null is an invalid segment for URI test", e.getMessage()); } } } diff --git a/src/test/java/com/networknt/schema/Issue396Test.java b/src/test/java/com/networknt/schema/Issue396Test.java index acffaf1e6..54b8f5dd1 100644 --- a/src/test/java/com/networknt/schema/Issue396Test.java +++ b/src/test/java/com/networknt/schema/Issue396Test.java @@ -39,7 +39,7 @@ public void testComplexPropertyNamesV7() throws Exception { }); Set errors = schema.validate(node); - final Set actual = errors.stream().map(ValidationMessage::getPath).collect(Collectors.toSet()); + final Set actual = errors.stream().map(ValidationMessage::getInstanceLocation).map(Object::toString).collect(Collectors.toSet()); Assertions.assertEquals(expected, actual); } } diff --git a/src/test/java/com/networknt/schema/Issue451Test.java b/src/test/java/com/networknt/schema/Issue451Test.java index 186bdb7ac..8edc17842 100644 --- a/src/test/java/com/networknt/schema/Issue451Test.java +++ b/src/test/java/com/networknt/schema/Issue451Test.java @@ -60,16 +60,18 @@ private void walk(JsonNode data, boolean shouldValidate) { CollectorContext collectorContext = schema.walk(data, shouldValidate).getCollectorContext(); Map collector = (Map) collectorContext.get(COLLECTOR_ID); - Assertions.assertEquals(2, collector.get("#/definitions/definition1/properties/a")); - Assertions.assertEquals(2, collector.get("#/definitions/definition2/properties/x")); + Assertions.assertEquals(2, + collector.get("https://example.com/issue-451.json#/definitions/definition1/properties/a")); + Assertions.assertEquals(2, + collector.get("https://example.com/issue-451.json#/definitions/definition2/properties/x")); } private static class CountingWalker implements JsonSchemaWalkListener { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - String path = walkEvent.getSchemaPath(); - collector(walkEvent.getExecutionContext()).compute(path, (k, v) -> v == null ? 1 : v + 1); + SchemaLocation path = walkEvent.getSchemaLocation(); + collector(walkEvent.getExecutionContext()).compute(path.toString(), (k, v) -> v == null ? 1 : v + 1); return WalkFlow.CONTINUE; } diff --git a/src/test/java/com/networknt/schema/Issue467Test.java b/src/test/java/com/networknt/schema/Issue467Test.java new file mode 100644 index 000000000..45a52db1a --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue467Test.java @@ -0,0 +1,92 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.walk.JsonSchemaWalkListener; +import com.networknt.schema.walk.WalkEvent; +import com.networknt.schema.walk.WalkFlow; + +public class Issue467Test { + private static JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); + private static String schemaPath = "/schema/issue467.json"; + + protected ObjectMapper mapper = new ObjectMapper(); + + @Test + public void shouldWalkKeywordWithValidation() throws URISyntaxException, IOException { + InputStream schemaInputStream = Issue467Test.class.getResourceAsStream(schemaPath); + final Set properties = new LinkedHashSet<>(); + final SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new JsonSchemaWalkListener() { + @Override + public WalkFlow onWalkStart(WalkEvent walkEvent) { + properties.add(walkEvent.getEvaluationPath()); + return WalkFlow.CONTINUE; + } + + @Override + public void onWalkEnd(WalkEvent walkEvent, Set set) { + } + }); + JsonSchema schema = factory.getSchema(schemaInputStream, config); + JsonNode data = mapper.readTree(Issue467Test.class.getResource("/data/issue467.json")); + ValidationResult result = schema.walk(data, true); + assertEquals(new HashSet<>(Arrays.asList("$.properties", "$.properties.tags.items[0].properties")), + properties.stream().map(Object::toString).collect(Collectors.toSet())); + assertEquals(1, result.getValidationMessages().size()); + } + + @Test + public void shouldWalkPropertiesWithValidation() throws URISyntaxException, IOException { + InputStream schemaInputStream = Issue467Test.class.getResourceAsStream(schemaPath); + final Set properties = new LinkedHashSet<>(); + final SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.addPropertyWalkListener(new JsonSchemaWalkListener() { + @Override + public WalkFlow onWalkStart(WalkEvent walkEvent) { + properties.add(walkEvent.getEvaluationPath()); + return WalkFlow.CONTINUE; + } + + @Override + public void onWalkEnd(WalkEvent walkEvent, Set set) { + } + }); + JsonSchema schema = factory.getSchema(schemaInputStream, config); + JsonNode data = mapper.readTree(Issue467Test.class.getResource("/data/issue467.json")); + ValidationResult result = schema.walk(data, true); + assertEquals( + new HashSet<>(Arrays.asList("$.properties.tags", "$.properties.tags.items[0].properties.category")), + properties.stream().map(Object::toString).collect(Collectors.toSet())); + assertEquals(1, result.getValidationMessages().size()); + } + +} diff --git a/src/test/java/com/networknt/schema/Issue471Test.java b/src/test/java/com/networknt/schema/Issue471Test.java index 836eb3bb2..c74108d4f 100644 --- a/src/test/java/com/networknt/schema/Issue471Test.java +++ b/src/test/java/com/networknt/schema/Issue471Test.java @@ -74,7 +74,7 @@ private Map validate() throws Exception { } private Map convertValidationMessagesToMap(Set validationMessages) { - return validationMessages.stream().collect(Collectors.toMap(ValidationMessage::getPath, ValidationMessage::getMessage)); + return validationMessages.stream().collect(Collectors.toMap(m -> m.getInstanceLocation().toString(), ValidationMessage::getMessage)); } private JsonSchema getJsonSchemaFromStreamContentV201909(InputStream schemaContent) { diff --git a/src/test/java/com/networknt/schema/Issue550Test.java b/src/test/java/com/networknt/schema/Issue550Test.java index fc8e9fce8..741b2747f 100644 --- a/src/test/java/com/networknt/schema/Issue550Test.java +++ b/src/test/java/com/networknt/schema/Issue550Test.java @@ -33,7 +33,7 @@ void testValidationMessageDoContainSchemaPath() throws Exception { Set errors = schema.validate(node); ValidationMessage validationMessage = errors.stream().findFirst().get(); - Assertions.assertEquals("#/properties/age/minimum", validationMessage.getSchemaPath()); + Assertions.assertEquals("https://example.com/person.schema.json#/properties/age/minimum", validationMessage.getSchemaLocation().toString()); Assertions.assertEquals(1, errors.size()); } @@ -48,7 +48,7 @@ void testValidationMessageDoContainSchemaPathForOneOf() throws Exception { ValidationMessage validationMessage = errors.stream().findFirst().get(); // Instead of capturing all subSchema within oneOf, a pointer to oneOf should be provided. - Assertions.assertEquals("#/oneOf", validationMessage.getSchemaPath()); + Assertions.assertEquals("https://example.com/person.schema.json#/oneOf", validationMessage.getSchemaLocation().toString()); Assertions.assertEquals(1, errors.size()); } diff --git a/src/test/java/com/networknt/schema/Issue662Test.java b/src/test/java/com/networknt/schema/Issue662Test.java index 1535f0aa6..94ebb3f2e 100644 --- a/src/test/java/com/networknt/schema/Issue662Test.java +++ b/src/test/java/com/networknt/schema/Issue662Test.java @@ -9,7 +9,6 @@ import java.util.Set; import static java.util.stream.Collectors.toList; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; public class Issue662Test extends BaseJsonSchemaValidatorTest { @@ -41,17 +40,18 @@ void testCorrectErrorForInvalidValue() throws IOException { JsonNode node = getJsonNodeFromClasspath(resource("objectInvalidValue.json")); Set errors = schema.validate(node); List errorMessages = errors.stream() - .map(ValidationMessage::getMessage) + .map(v -> v.getEvaluationPath() + " = " + v.getMessage()) .collect(toList()); - assertTrue( - errorMessages.contains("$.optionalObject.value: does not have a value in the enumeration [one, two]"), - "Validation error for invalid object property is captured" - ); - assertFalse( - errorMessages.contains("$.optionalObject: object found, null expected"), - "No validation error that the object is not expected" - ); + // As this is from an anyOf evaluation both error messages should be present as they didn't match any + // The evaluation cannot be expected to know the semantic meaning that this is an optional object + // The evaluation path can be used to provide clarity on the reason + // Omitting the 'object found, null expected' message also provides the misleading impression that the + // object is required when leaving it empty is a possible option + assertTrue(errorMessages + .contains("$.properties.optionalObject.anyOf[0].type = $.optionalObject: object found, null expected")); + assertTrue(errorMessages.contains( + "$.properties.optionalObject.anyOf[1].properties.value.enum = $.optionalObject.value: does not have a value in the enumeration [one, two]")); } private static String resource(String name) { diff --git a/src/test/java/com/networknt/schema/Issue664Test.java b/src/test/java/com/networknt/schema/Issue664Test.java index 33fd98095..7056ab223 100644 --- a/src/test/java/com/networknt/schema/Issue664Test.java +++ b/src/test/java/com/networknt/schema/Issue664Test.java @@ -31,7 +31,8 @@ void shouldHaveFullSchemaPaths() throws Exception { JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream); InputStream dataInputStream = getClass().getResourceAsStream(dataPath); JsonNode node = getJsonNodeFromStreamContent(dataInputStream); - List errorSchemaPaths = schema.validate(node).stream().map(ValidationMessage::getSchemaPath).collect(Collectors.toList()); + List errorSchemaPaths = schema.validate(node).stream().map(ValidationMessage::getSchemaLocation) + .map(Object::toString).collect(Collectors.toList()); List expectedSchemaPaths = Arrays.asList( "#/items/allOf/0/anyOf/0/oneOf", diff --git a/src/test/java/com/networknt/schema/Issue687Test.java b/src/test/java/com/networknt/schema/Issue687Test.java index fe44275bb..962105098 100644 --- a/src/test/java/com/networknt/schema/Issue687Test.java +++ b/src/test/java/com/networknt/schema/Issue687Test.java @@ -87,7 +87,7 @@ void testValidationMessage(PathType pathType, String schemaPath, String content, Set messages = schema.validate(new ObjectMapper().readTree(content)); assertEquals(expectedMessagePaths.length, messages.size()); for (String expectedPath: expectedMessagePaths) { - assertTrue(messages.stream().anyMatch(msg -> expectedPath.equals(msg.getPath()))); + assertTrue(messages.stream().anyMatch(msg -> expectedPath.equals(msg.getInstanceLocation().toString()))); } } @@ -128,7 +128,7 @@ void testSpecialCharacters(PathType pathType, String propertyName, String expect "}"), schemaValidatorsConfig); Set validationMessages = schema.validate(mapper.readTree("{\""+propertyName+"\": 1}")); assertEquals(1, validationMessages.size()); - assertEquals(expectedPath, validationMessages.iterator().next().getPath()); + assertEquals(expectedPath, validationMessages.iterator().next().getInstanceLocation().toString()); } } diff --git a/src/test/java/com/networknt/schema/Issue824Test.java b/src/test/java/com/networknt/schema/Issue824Test.java new file mode 100644 index 000000000..164420b2d --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue824Test.java @@ -0,0 +1,69 @@ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.uri.URITranslator; + +public class Issue824Test { + @Test + void validate() throws JsonProcessingException { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.addUriTranslator(URITranslator.prefix("https://json-schema.org", "resource:")); + final JsonSchema v201909SpecSchema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909) + .getSchema(URI.create(JsonMetaSchema.getV201909().getUri()), config); + v201909SpecSchema.preloadJsonSchema(); + final JsonNode invalidSchema = new ObjectMapper().readTree( + "{"+ + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\","+ + " \"type\": \"cat\" "+ + "}"); + + // Validate same JSON schema against v2019-09 spec schema twice + final Set validationErrors1 = v201909SpecSchema.validate(invalidSchema); + final Set validationErrors2 = v201909SpecSchema.validate(invalidSchema); + + // Validation errors should be the same + assertEquals(validationErrors1, validationErrors2); + + // Results + // + // 1.0.73 + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: should be valid to any of the schemas + // array] + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: should be valid to any of the schemas + // array] + // + // 1.0.74 + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: string found, array expected] + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: string found, array expected] + // + // 1.0.78 + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: should be valid to any of the schemas + // array] + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: should be valid to any of the schemas + // array] + // + // >= 1.0.82 + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: string found, array expected] + // [$.type: does not have a value in the enumeration [array, boolean, integer, + // null, number, object, string], $.type: should be valid to any of the schemas + // array] + // + // ????? + } +} diff --git a/src/test/java/com/networknt/schema/JsonNodePathTest.java b/src/test/java/com/networknt/schema/JsonNodePathTest.java new file mode 100644 index 000000000..9f91ca91f --- /dev/null +++ b/src/test/java/com/networknt/schema/JsonNodePathTest.java @@ -0,0 +1,137 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +class JsonNodePathTest { + + @Test + void getNameCount() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath path = root.append("hello").append("world"); + assertEquals(2, path.getNameCount()); + } + + @Test + void getName() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath path = root.append("hello").append("world"); + assertEquals("hello", path.getName(0)); + assertEquals("world", path.getName(1)); + assertEquals("world", path.getName(-1)); + assertThrows(IllegalArgumentException.class, () -> path.getName(2)); + } + + @Test + void compareTo() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath a = root.append("a"); + JsonNodePath aa = a.append("a"); + + JsonNodePath b = root.append("b"); + JsonNodePath bb = b.append("b"); + JsonNodePath b1 = b.append(1); + JsonNodePath bbb = bb.append("b"); + + JsonNodePath c = root.append("c"); + JsonNodePath cc = c.append("c"); + + List paths = new ArrayList<>(); + paths.add(cc); + paths.add(aa); + paths.add(bb); + + paths.add(b1); + + paths.add(bbb); + + paths.add(b); + paths.add(a); + paths.add(c); + + Collections.sort(paths); + + String[] result = paths.stream().map(Object::toString).collect(Collectors.toList()).toArray(new String[0]); + + assertArrayEquals(new String[] { "/a", "/b", "/c", "/a/a", "/b/1", "/b/b", "/c/c", "/b/b/b" }, result); + } + + @Test + void equalsEquals() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath a1 = root.append("a"); + JsonNodePath a2 = root.append("a"); + assertEquals(a1, a2); + } + + @Test + void hashCodeEquals() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath a1 = root.append("a"); + JsonNodePath a2 = root.append("a"); + assertEquals(a1.hashCode(), a2.hashCode()); + } + + @Test + void getPathType() { + JsonNodePath root = new JsonNodePath(PathType.JSON_POINTER); + assertEquals(PathType.JSON_POINTER, root.getPathType()); + } + + @Test + void getElement() { + JsonNodePath root = new JsonNodePath(PathType.JSON_PATH); + JsonNodePath path = root.append("hello").append(1).append("world"); + assertEquals("hello", path.getElement(0)); + assertEquals(Integer.valueOf(1), path.getElement(1)); + assertEquals("world", path.getElement(2)); + assertEquals("world", path.getElement(-1)); + assertEquals("$.hello[1].world", path.toString()); + assertThrows(IllegalArgumentException.class, () -> path.getName(3)); + } + + @Test + void startsWith() { + JsonNodePath root = new JsonNodePath(PathType.JSON_PATH); + JsonNodePath path = root.append("items"); + JsonNodePath other = root.append("unevaluatedItems"); + assertTrue(path.startsWith(other.getParent())); + + path = root.append("allOf").append(0).append("items"); + other = root.append("allOf").append(1).append("unevaluatedItems"); + assertFalse(path.startsWith(other.getParent())); + + path = root.append("allOf").append(0).append("items"); + other = root.append("allOf").append(0).append("unevaluatedItems"); + assertTrue(path.startsWith(other.getParent())); + + path = root.append("items"); + other = root.append("items").append(0); + assertTrue(path.startsWith(other.getParent())); + + path = root.append("allOf"); + other = root.append("allOf").append(0).append("items"); + assertFalse(path.startsWith(other.getParent())); + } +} diff --git a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java index c63655ae3..0c95579f2 100644 --- a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java @@ -108,12 +108,12 @@ void testApplyDefaults0(String method) throws IOException { throw new UnsupportedOperationException(); } assertThat(validationMessages.stream().map(ValidationMessage::getMessage).collect(Collectors.toList()), - Matchers.containsInAnyOrder("$.outer.mixedObject.intValue_missing: is missing but it is required", - "$.outer.mixedObject.intValue_missingButError: is missing but it is required", + Matchers.containsInAnyOrder("$.outer.mixedObject: required property 'intValue_missing' not found", + "$.outer.mixedObject: required property 'intValue_missingButError' not found", "$.outer.mixedObject.intValue_null: null found, integer expected", "$.outer.goodArray[1]: null found, string expected", "$.outer.badArray[1]: null found, string expected", - "$.outer.reference.stringValue_missing: is missing but it is required")); + "$.outer.reference: required property 'stringValue_missing' not found")); assertEquals(inputNodeOriginal, inputNode); } diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java index 8989b94f1..50a14608b 100644 --- a/src/test/java/com/networknt/schema/JsonWalkTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkTest.java @@ -124,10 +124,10 @@ public String getValue() { } @Override - public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext) throws JsonSchemaException { + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(); + return new CustomValidator(schemaLocation, evaluationPath); } return null; } @@ -138,21 +138,20 @@ public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSc * This will be helpful in cases where we don't want to revisit the entire JSON * document again just for gathering this kind of information. */ - private static class CustomValidator implements JsonValidator { + private static class CustomValidator extends AbstractJsonValidator { - @Override - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, String at) { - return new TreeSet(); + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { + super(schemaLocation, evaluationPath,null); } @Override - public Set validate(ExecutionContext executionContext, JsonNode rootNode) { - return validate(executionContext, rootNode, rootNode, PathType.DEFAULT.getRoot()); + public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + return new TreeSet(); } @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - String at, boolean shouldValidateSchema) { + JsonNodePath instanceLocation, boolean shouldValidateSchema) { return new LinkedHashSet(); } } @@ -162,7 +161,7 @@ private static class AllKeywordListener implements JsonSchemaWalkListener { @Override public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { ObjectMapper mapper = new ObjectMapper(); - String keyWordName = keywordWalkEvent.getKeyWordName(); + String keyWordName = keywordWalkEvent.getKeyword(); JsonNode schemaNode = keywordWalkEvent.getSchemaNode(); CollectorContext collectorContext = keywordWalkEvent.getExecutionContext().getCollectorContext(); if (collectorContext.get(SAMPLE_WALK_COLLECTOR_TYPE) == null) { diff --git a/src/test/java/com/networknt/schema/OutputFormatTest.java b/src/test/java/com/networknt/schema/OutputFormatTest.java new file mode 100644 index 000000000..48c46975a --- /dev/null +++ b/src/test/java/com/networknt/schema/OutputFormatTest.java @@ -0,0 +1,51 @@ +package com.networknt.schema; + +import static org.hamcrest.MatcherAssert.assertThat; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +class OutputFormatTest { + + private static JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + private static String schemaPath1 = "/schema/output-format-schema.json"; + + private JsonNode getJsonNodeFromJsonData(String jsonFilePath) throws Exception { + InputStream content = getClass().getResourceAsStream(jsonFilePath); + ObjectMapper mapper = new ObjectMapper(); + return mapper.readTree(content); + } + + @Test + @DisplayName("Test Validation Messages") + void testInvalidJson() throws Exception { + InputStream schemaInputStream = OutputFormatTest.class.getResourceAsStream(schemaPath1); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaInputStream, config); + JsonNode node = getJsonNodeFromJsonData("/data/output-format-input.json"); + Set errors = schema.validate(node); + Assertions.assertEquals(3, errors.size()); + + Set messages = errors.stream().map(m -> new String[] { m.getEvaluationPath().toString(), + m.getSchemaLocation().toString(), m.getInstanceLocation().toString(), m.getMessage() }) + .collect(Collectors.toSet()); + + assertThat(messages, + Matchers.containsInAnyOrder( + new String[] { "/minItems", "https://example.com/polygon#/minItems", "", ": expected at least 3 items but found 2" }, + new String[] { "/items/$ref/additionalProperties", "https://example.com/polygon#/$defs/point/additionalProperties", "/1/z", + "/1/z: is not defined in the schema and the schema does not allow additional properties" }, + new String[] { "/items/$ref/required", "https://example.com/polygon#/$defs/point/required", "/1", "/1: required property 'y' not found"})); + } +} diff --git a/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java index f8e208163..a84d4ff6e 100644 --- a/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java +++ b/src/test/java/com/networknt/schema/OverwritingCustomMessageBugTest.java @@ -45,7 +45,7 @@ private Set validate() throws Exception { private Map transferErrorMsg(Set validationMessages) { Map pathToMessage = new HashMap<>(); validationMessages.forEach(msg -> { - pathToMessage.put(msg.getPath(), msg.getMessage()); + pathToMessage.put(msg.getInstanceLocation().toString(), msg.getMessage()); }); return pathToMessage; } diff --git a/src/test/java/com/networknt/schema/PropertiesTest.java b/src/test/java/com/networknt/schema/PropertiesTest.java new file mode 100644 index 000000000..aaca8cb73 --- /dev/null +++ b/src/test/java/com/networknt/schema/PropertiesTest.java @@ -0,0 +1,20 @@ +package com.networknt.schema; + +import com.networknt.schema.SpecVersion.VersionFlag; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.TestFactory; + +import java.util.stream.Stream; + +@DisplayName("Properties") +public class PropertiesTest extends AbstractJsonSchemaTestSuite { + + @TestFactory + @DisplayName("Draft 2019-09") + Stream draft201909() { + return createTests(VersionFlag.V201909, "src/test/resources/draft2019-09/properties.json"); + } + +} diff --git a/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java b/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java index 6f95cbaa8..819d122dd 100644 --- a/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java +++ b/src/test/java/com/networknt/schema/RecursiveReferenceValidatorExceptionTest.java @@ -26,7 +26,7 @@ void testInvalidRecursiveReference() { // Act and Assert assertThrows(JsonSchemaException.class, () -> { - new RecursiveRefValidator("#", schemaNode, null, validationContext); + new RecursiveRefValidator(SchemaLocation.of(""), new JsonNodePath(PathType.JSON_POINTER), schemaNode, null, validationContext); }); } diff --git a/src/test/java/com/networknt/schema/SchemaLocationTest.java b/src/test/java/com/networknt/schema/SchemaLocationTest.java new file mode 100644 index 000000000..897f0b2ce --- /dev/null +++ b/src/test/java/com/networknt/schema/SchemaLocationTest.java @@ -0,0 +1,206 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class SchemaLocationTest { + + @Test + void ofAbsoluteIri() { + SchemaLocation schemaLocation = SchemaLocation.of("https://json-schema.org/draft/2020-12/schema"); + assertEquals("https://json-schema.org/draft/2020-12/schema", schemaLocation.getAbsoluteIri().toString()); + assertEquals(0, schemaLocation.getFragment().getNameCount()); + assertEquals("https://json-schema.org/draft/2020-12/schema#", schemaLocation.toString()); + } + + @Test + void ofAbsoluteIriWithJsonPointer() { + SchemaLocation schemaLocation = SchemaLocation.of("https://json-schema.org/draft/2020-12/schema#/properties/0"); + assertEquals("https://json-schema.org/draft/2020-12/schema", schemaLocation.getAbsoluteIri().toString()); + assertEquals(2, schemaLocation.getFragment().getNameCount()); + assertEquals("https://json-schema.org/draft/2020-12/schema#/properties/0", schemaLocation.toString()); + assertEquals("/properties/0", schemaLocation.getFragment().toString()); + } + + @Test + void ofAbsoluteIriWithAnchor() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address", schemaLocation.getAbsoluteIri().toString()); + assertEquals(1, schemaLocation.getFragment().getNameCount()); + assertEquals("https://example.com/schemas/address#street_address", schemaLocation.toString()); + assertEquals("street_address", schemaLocation.getFragment().toString()); + } + + @Test + void ofDocument() { + assertEquals(SchemaLocation.DOCUMENT, SchemaLocation.of("#")); + } + + @Test + void document() { + assertEquals(SchemaLocation.DOCUMENT.toString(), "#"); + } + + @Test + void schemaLocationResolveDocument() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#", SchemaLocation.resolve(schemaLocation, "#")); + } + + @Test + void schemaLocationResolveDocumentPointer() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#/allOf/12/properties", + SchemaLocation.resolve(schemaLocation, "#/allOf/12/properties")); + } + + @Test + void schemaLocationResolveEmptyString() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#", SchemaLocation.resolve(schemaLocation, "")); + } + + @Test + void schemaLocationResolveRelative() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/test#", SchemaLocation.resolve(schemaLocation, "test")); + } + + @Test + void schemaLocationResolveRelativeIndex() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address/#street_address"); + assertEquals("https://example.com/schemas/address/test#", SchemaLocation.resolve(schemaLocation, "test")); + } + + @Test + void resolveDocument() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#", schemaLocation.resolve("#").toString()); + } + + @Test + void resolveDocumentPointer() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#/allOf/10/properties", + schemaLocation.resolve("#/allOf/10/properties").toString()); + } + + @Test + void resolveEmptyString() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/address#", schemaLocation.resolve("").toString()); + } + + @Test + void resolveRelative() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address#street_address"); + assertEquals("https://example.com/schemas/test#", schemaLocation.resolve("test").toString()); + } + + @Test + void resolveRelativeIndex() { + SchemaLocation schemaLocation = SchemaLocation.of("https://example.com/schemas/address/#street_address"); + assertEquals("https://example.com/schemas/address/test#", schemaLocation.resolve("test").toString()); + } + + @Test + void resolveNull() { + SchemaLocation schemaLocation = new SchemaLocation(null); + assertEquals("test#", schemaLocation.resolve("test").toString()); + } + + @Test + void build() { + SchemaLocation schemaLocation = SchemaLocation.builder().absoluteIri("https://example.com/schemas/address/") + .fragment("/allOf/10/properties").build(); + assertEquals("https://example.com/schemas/address/#/allOf/10/properties", schemaLocation.toString()); + assertEquals("https://example.com/schemas/address/", schemaLocation.getAbsoluteIri().toString()); + assertEquals("/allOf/10/properties", schemaLocation.getFragment().toString()); + } + + @Test + void append() { + SchemaLocation schemaLocation = SchemaLocation.builder().absoluteIri("https://example.com/schemas/address/") + .build().append("allOf").append(10).append("properties"); + assertEquals("https://example.com/schemas/address/#/allOf/10/properties", schemaLocation.toString()); + assertEquals("https://example.com/schemas/address/", schemaLocation.getAbsoluteIri().toString()); + assertEquals("/allOf/10/properties", schemaLocation.getFragment().toString()); + } + + @Test + void anchorFragment() { + assertTrue(SchemaLocation.Fragment.isAnchorFragment("#test")); + assertFalse(SchemaLocation.Fragment.isAnchorFragment("#")); + assertFalse(SchemaLocation.Fragment.isAnchorFragment("#/allOf/10/properties")); + assertFalse(SchemaLocation.Fragment.isAnchorFragment("")); + } + + @Test + void jsonPointerFragment() { + assertTrue(SchemaLocation.Fragment.isJsonPointerFragment("#/allOf/10/properties")); + assertFalse(SchemaLocation.Fragment.isJsonPointerFragment("#")); + assertFalse(SchemaLocation.Fragment.isJsonPointerFragment("#test")); + } + + @Test + void fragment() { + assertTrue(SchemaLocation.Fragment.isFragment("#/allOf/10/properties")); + assertTrue(SchemaLocation.Fragment.isFragment("#test")); + assertFalse(SchemaLocation.Fragment.isFragment("test")); + } + + @Test + void documentFragment() { + assertFalse(SchemaLocation.Fragment.isDocumentFragment("#/allOf/10/properties")); + assertFalse(SchemaLocation.Fragment.isDocumentFragment("#test")); + assertFalse(SchemaLocation.Fragment.isDocumentFragment("test")); + assertTrue(SchemaLocation.Fragment.isDocumentFragment("#")); + } + + @Test + void ofNull() { + assertNull(SchemaLocation.of(null)); + } + + @Test + void ofEmptyString() { + SchemaLocation schemaLocation = SchemaLocation.of(""); + assertEquals("", schemaLocation.getAbsoluteIri().toString()); + assertEquals("#", schemaLocation.toString()); + } + + @Test + void newNull() { + SchemaLocation schemaLocation = new SchemaLocation(null); + assertEquals("#", schemaLocation.toString()); + } + + @Test + void equalsEquals() { + assertEquals(SchemaLocation.of("https://example.com/schemas/address/#street_address"), + SchemaLocation.of("https://example.com/schemas/address/#street_address")); + } + + @Test + void hashCodeEquals() { + assertEquals(SchemaLocation.of("https://example.com/schemas/address/#street_address").hashCode(), + SchemaLocation.of("https://example.com/schemas/address/#street_address").hashCode()); + } + +} diff --git a/src/test/resources/data/issue467.json b/src/test/resources/data/issue467.json new file mode 100644 index 000000000..7be04dbb5 --- /dev/null +++ b/src/test/resources/data/issue467.json @@ -0,0 +1,19 @@ +{ +"tags": [ + { + "category": "book" + }, + { + "value": "2", + "category": "book" + }, + { + "value": "3", + "category": "book" + }, + { + "value": "4", + "category": "book" + } +] +} diff --git a/src/test/resources/data/output-format-input.json b/src/test/resources/data/output-format-input.json new file mode 100644 index 000000000..8bdb2103f --- /dev/null +++ b/src/test/resources/data/output-format-input.json @@ -0,0 +1,10 @@ +[ + { + "x": 2.5, + "y": 1.3 + }, + { + "x": 1, + "z": 6.7 + } +] \ No newline at end of file diff --git a/src/test/resources/draft2019-09/invalid-min-max-contains.json b/src/test/resources/draft2019-09/invalid-min-max-contains.json index 73e8583f8..011522104 100644 --- a/src/test/resources/draft2019-09/invalid-min-max-contains.json +++ b/src/test/resources/draft2019-09/invalid-min-max-contains.json @@ -11,7 +11,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":\"1\"}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":\"1\"}" ] } ] @@ -28,7 +28,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":0.5}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":0.5}" ] } ] @@ -45,7 +45,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":-1}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"minContains\":-1}" ] } ] @@ -62,7 +62,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":\"1\"}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":\"1\"}" ] } ] @@ -79,7 +79,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0.5}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0.5}" ] } ] @@ -96,7 +96,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":-1}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":-1}" ] } ] @@ -114,8 +114,8 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0,\"minContains\":1}", - "#/minContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0,\"minContains\":1}" + "/maxContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0,\"minContains\":1}", + "/minContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2019-09/schema\",\"maxContains\":0,\"minContains\":1}" ] } ] diff --git a/src/test/resources/draft2019-09/properties.json b/src/test/resources/draft2019-09/properties.json new file mode 100644 index 000000000..6c96d8922 --- /dev/null +++ b/src/test/resources/draft2019-09/properties.json @@ -0,0 +1,32 @@ +[ + { + "description": "object properties validation with required", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "string"}, + "hello": {"type": "string"}, + "world": {"type": "string"} + }, + "required": [ "bar", "hello", "world" ] + }, + "tests": [ + { + "description": "required hello and world is not present", + "data": {"foo": 1, "bar": "baz"}, + "valid": false + }, + { + "description": "one property invalid is invalid", + "data": {"foo": 1, "bar": {}, "hello": "v", "world": "b"}, + "valid": false + }, + { + "description": "all valid", + "data": {"foo": 1, "bar": "b", "hello": "c", "world": "d"}, + "valid": true + } + ] + } +] diff --git a/src/test/resources/draft2020-12/invalid-min-max-contains.json b/src/test/resources/draft2020-12/invalid-min-max-contains.json index 54ceea418..c19bdec70 100644 --- a/src/test/resources/draft2020-12/invalid-min-max-contains.json +++ b/src/test/resources/draft2020-12/invalid-min-max-contains.json @@ -11,7 +11,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":\"1\"}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":\"1\"}" ] } ] @@ -28,7 +28,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":0.5}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":0.5}" ] } ] @@ -45,7 +45,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":-1}" + "/minContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"minContains\":-1}" ] } ] @@ -62,7 +62,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":\"1\"}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":\"1\"}" ] } ] @@ -79,7 +79,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0.5}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0.5}" ] } ] @@ -96,7 +96,7 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":-1}" + "/maxContains: must be a non-negative integer in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":-1}" ] } ] @@ -114,8 +114,8 @@ "data": [], "valid": false, "validationMessages": [ - "#/maxContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0,\"minContains\":1}", - "#/minContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0,\"minContains\":1}" + "/maxContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0,\"minContains\":1}", + "/minContains: minContains must less than or equal to maxContains in {\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"maxContains\":0,\"minContains\":1}" ] } ] diff --git a/src/test/resources/draft7/issue491.json b/src/test/resources/draft7/issue491.json index a15fbf680..afcdc0910 100644 --- a/src/test/resources/draft7/issue491.json +++ b/src/test/resources/draft7/issue491.json @@ -82,7 +82,7 @@ }, "valid": false, "validationMessages": [ - "$.search.name: is missing but it is required", + "$.search: required property 'name' not found", "$.search.searchAge.age: string found, integer expected", "$.search: should be valid to one and only one schema, but 0 are valid" ] @@ -97,7 +97,7 @@ "valid": false, "validationMessages": [ "$.search.name: integer found, string expected", - "$.search.searchAge: is missing but it is required", + "$.search: required property 'searchAge' not found", "$.search: should be valid to one and only one schema, but 0 are valid" ] } @@ -184,7 +184,7 @@ }, "valid": false, "validationMessages": [ - "$.search.name: is missing but it is required", + "$.search: required property 'name' not found", "$.search.byAge.age: string found, integer expected", "$.search: should be valid to one and only one schema, but 0 are valid" ] @@ -199,7 +199,7 @@ "valid": false, "validationMessages": [ "$.search.name: integer found, string expected", - "$.search.byAge: is missing but it is required", + "$.search: required property 'byAge' not found", "$.search: should be valid to one and only one schema, but 0 are valid" ] } @@ -276,7 +276,7 @@ }, "valid": false, "validationMessages": [ - "$.search.name: is missing but it is required", + "$.search: required property 'name' not found", "$.search.age: string found, integer expected", "$.search: should be valid to one and only one schema, but 0 are valid" ] @@ -291,7 +291,7 @@ "valid": false, "validationMessages": [ "$.search.name: integer found, string expected", - "$.search.age: is missing but it is required", + "$.search: required property 'age' not found", "$.search: should be valid to one and only one schema, but 0 are valid" ] }, @@ -304,7 +304,7 @@ }, "valid": false, "validationMessages": [ - "$.search.name: is missing but it is required", + "$.search: required property 'name' not found", "$.search.age: must have a maximum value of 150", "$.search: should be valid to one and only one schema, but 0 are valid" ] @@ -319,7 +319,7 @@ "valid": false, "validationMessages": [ "$.search.name: may only be 20 characters long", - "$.search.age: is missing but it is required", + "$.search: required property 'age' not found", "$.search: should be valid to one and only one schema, but 0 are valid" ] } diff --git a/src/test/resources/draft7/issue516.json b/src/test/resources/draft7/issue516.json index c9c33f299..11430471b 100644 --- a/src/test/resources/draft7/issue516.json +++ b/src/test/resources/draft7/issue516.json @@ -121,22 +121,22 @@ "validationMessages": [ "$.activities[0]: should be valid to one and only one schema, but 0 are valid", "$.activities[0].age: is not defined in the schema and the schema does not allow additional properties", - "$.activities[0].chemicalCharacteristic: is missing but it is required", + "$.activities[0]: required property 'chemicalCharacteristic' not found", "$.activities[0].height: is not defined in the schema and the schema does not allow additional properties", - "$.activities[0].weight: is missing but it is required", + "$.activities[0]: required property 'weight' not found", "$.activities[1]: should be valid to one and only one schema, but 0 are valid", "$.activities[1].chemicalCharacteristic: is not defined in the schema and the schema does not allow additional properties", - "$.activities[1].height: is missing but it is required", + "$.activities[1]: required property 'height' not found", "$.activities[1].toxic: is not defined in the schema and the schema does not allow additional properties", - "$.activities[1].weight: is missing but it is required", + "$.activities[1]: required property 'weight' not found", "$.activities[2]: should be valid to one and only one schema, but 0 are valid", "$.activities[2].chemicalCharacteristic: is not defined in the schema and the schema does not allow additional properties", "$.activities[2].chemicalCharacteristic: should be valid to one and only one schema, but 0 are valid", "$.activities[2].chemicalCharacteristic.categoryName: is not defined in the schema and the schema does not allow additional properties", "$.activities[2].chemicalCharacteristic.name: is not defined in the schema and the schema does not allow additional properties", - "$.activities[2].height: is missing but it is required", + "$.activities[2]: required property 'height' not found", "$.activities[2].toxic: is not defined in the schema and the schema does not allow additional properties", - "$.activities[2].weight: is missing but it is required" + "$.activities[2]: required property 'weight' not found" ] } ] diff --git a/src/test/resources/draft7/issue653.json b/src/test/resources/draft7/issue653.json index c576bf73a..1fe83d156 100644 --- a/src/test/resources/draft7/issue653.json +++ b/src/test/resources/draft7/issue653.json @@ -81,7 +81,7 @@ "valid": false, "validationMessages": [ "$.pets[0]: should be valid to one and only one schema, but 0 are valid", - "$.pets[0].age: is missing but it is required", + "$.pets[0]: required property 'age' not found", "Boolean schema false is not valid" ] } diff --git a/src/test/resources/draft7/issue678.json b/src/test/resources/draft7/issue678.json index b8873ce5c..30b0cfece 100644 --- a/src/test/resources/draft7/issue678.json +++ b/src/test/resources/draft7/issue678.json @@ -50,8 +50,8 @@ "valid": false, "validationMessages": [ "$.outerObject.innerObject: object found, string expected", - "$.outerObject.innerObject.value: is missing but it is required", - "$.outerObject.innerObject.unit: is missing but it is required", + "$.outerObject.innerObject: required property 'value' not found", + "$.outerObject.innerObject: required property 'unit' not found", "$.outerObject.innerObject: should be valid to one and only one schema, but 0 are valid" ] } diff --git a/src/test/resources/schema/issue467.json b/src/test/resources/schema/issue467.json new file mode 100644 index 000000000..257ea1654 --- /dev/null +++ b/src/test/resources/schema/issue467.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "default_schema", + "description": "Default Description", + "properties": { + "tags": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "value" : { + "type": "string" + }, + "category" : { + "type": "string" + } + }, + "required": [ "value" ] + } + ] + } + }, + "required": [ "tags" ] +} \ No newline at end of file diff --git a/src/test/resources/schema/output-format-schema.json b/src/test/resources/schema/output-format-schema.json new file mode 100644 index 000000000..10d395785 --- /dev/null +++ b/src/test/resources/schema/output-format-schema.json @@ -0,0 +1,18 @@ +{ + "$id": "https://example.com/polygon", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "point": { + "type": "object", + "properties": { + "x": { "type": "number" }, + "y": { "type": "number" } + }, + "additionalProperties": false, + "required": [ "x", "y" ] + } + }, + "type": "array", + "items": { "$ref": "#/$defs/point" }, + "minItems": 3 +} \ No newline at end of file diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index 05867ff6a..dc7b2c56d 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -89,7 +89,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.invalid" + "$.invalid: must not have unevaluated properties" ] }, { @@ -106,7 +106,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.address.invalid" + "$.address.invalid: must not have unevaluated properties" ] }, { @@ -123,7 +123,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.address.invalid2" + "$.address.invalid2: must not have unevaluated properties" ] }, { @@ -142,7 +142,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.address.residence.invalid" + "$.address.residence.invalid: must not have unevaluated properties" ] } ] @@ -237,7 +237,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.wheels" + "$.vehicle.wheels: must not have unevaluated properties" ] }, { @@ -253,7 +253,8 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.pontoons\n $.vehicle.wings" + "$.vehicle.pontoons: must not have unevaluated properties", + "$.vehicle.wings: must not have unevaluated properties" ] }, { @@ -270,7 +271,9 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.invalid\n $.vehicle.pontoons\n $.vehicle.wings" + "$.vehicle.invalid: must not have unevaluated properties", + "$.vehicle.pontoons: must not have unevaluated properties", + "$.vehicle.wings: must not have unevaluated properties" ] }, { @@ -285,7 +288,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.invalid" + "$.vehicle.invalid: must not have unevaluated properties" ] } ] @@ -380,7 +383,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.unevaluated" + "$.vehicle.unevaluated: must not have unevaluated properties" ] }, { @@ -395,7 +398,7 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.unevaluated" + "$.vehicle.unevaluated: must not have unevaluated properties" ] }, { @@ -412,7 +415,8 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.vehicle.unevaluated\n $.vehicle.wings" + "$.vehicle.unevaluated: must not have unevaluated properties", + "$.vehicle.wings: must not have unevaluated properties" ] } ] @@ -509,8 +513,10 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.wings: is missing but it is required", - "There are unevaluated properties at the following paths $.vehicle.pontoons\n $.vehicle.unevaluated\n $.vehicle.wheels" + "$.vehicle: required property 'wings' not found", + "$.vehicle.pontoons: must not have unevaluated properties", + "$.vehicle.unevaluated: must not have unevaluated properties", + "$.vehicle.wheels: must not have unevaluated properties" ] } ] @@ -572,7 +578,8 @@ }, "valid": false, "validationMessages": [ - "There are unevaluated properties at the following paths $.age\n $.unevaluated" + "$.age: must not have unevaluated properties", + "$.unevaluated: must not have unevaluated properties" ] } ]