();
@@ -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