boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, boolean failFast,
+ JsonNode instanceNode, JsonNodePath instanceLocation, int bounds) {
+ String messageKey = "contains";
+ if (ValidatorTypeCode.MIN_CONTAINS.equals(validatorTypeCode)) {
+ messageKey = CONTAINS_MIN;
+ } else if (ValidatorTypeCode.MAX_CONTAINS.equals(validatorTypeCode)) {
+ messageKey = CONTAINS_MAX;
+ }
+ return Collections
+ .singleton(message().instanceNode(instanceNode).instanceLocation(instanceLocation).messageKey(messageKey)
+ .locale(locale).failFast(failFast).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString())
+ .code(validatorTypeCode.getErrorCode()).type(validatorTypeCode.getValue()).build());
+ }
+
+ /**
+ * Determine if annotations must be collected for evaluation.
+ *
+ * This will be collected regardless of whether it is needed for reporting.
+ *
+ * @return true if annotations must be collected for evaluation.
+ */
+ private boolean collectAnnotations() {
+ return hasUnevaluatedItemsValidator();
}
- 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());
+ private boolean hasUnevaluatedItemsValidator() {
+ if (this.hasUnevaluatedItemsValidator == null) {
+ this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems");
+ }
+ return hasUnevaluatedItemsValidator;
}
}
diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java
index 579e67921..03a51c31f 100644
--- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java
+++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java
@@ -17,15 +17,15 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Base64;
import java.util.Collections;
import java.util.Set;
-
/**
- * Validation for contentEncoding keyword.
+ * {@link JsonValidator} for contentEncoding.
*
* Note that since 2019-09 this keyword only generates annotations and not
* assertions.
@@ -34,6 +34,15 @@ public class ContentEncodingValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ContentEncodingValidator.class);
private String contentEncoding;
+ /**
+ * Constructor.
+ *
+ * @param schemaLocation the schema location
+ * @param evaluationPath the evaluation path
+ * @param schemaNode the schema node
+ * @param parentSchema the parent schema
+ * @param validationContext the validation context
+ */
public ContentEncodingValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_ENCODING,
@@ -64,10 +73,17 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (nodeType != JsonType.STRING) {
return Collections.emptySet();
}
+
+ if (collectAnnotations(executionContext)) {
+ putAnnotation(executionContext,
+ annotation -> annotation.instanceLocation(instanceLocation).value(this.contentEncoding));
+ }
if (!matches(node.asText())) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentEncoding).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.contentEncoding)
+ .build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java
index 38bf8780b..7eecf1c3a 100644
--- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java
+++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java
@@ -30,7 +30,7 @@
import com.networknt.schema.serialization.JsonMapperFactory;
/**
- * Validation for contentMediaType keyword.
+ * {@link JsonValidator} for contentMediaType.
*
* Note that since 2019-09 this keyword only generates annotations and not assertions.
*/
@@ -40,6 +40,15 @@ public class ContentMediaTypeValidator extends BaseJsonValidator {
private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING);
private final String contentMediaType;
+ /**
+ * Constructor.
+ *
+ * @param schemaLocation the schema location
+ * @param evaluationPath the evaluation path
+ * @param schemaNode the schema node
+ * @param parentSchema the parent schema
+ * @param validationContext the validation context
+ */
public ContentMediaTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_MEDIA_TYPE, validationContext);
@@ -89,9 +98,16 @@ public Set validate(ExecutionContext executionContext, JsonNo
return Collections.emptySet();
}
+ if (collectAnnotations(executionContext)) {
+ putAnnotation(executionContext,
+ annotation -> annotation.instanceLocation(instanceLocation).value(this.contentMediaType));
+ }
+
if (!matches(node.asText())) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentMediaType).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.contentMediaType)
+ .build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java
index 1415ab9f3..c36789511 100644
--- a/src/main/java/com/networknt/schema/DependenciesValidator.java
+++ b/src/main/java/com/networknt/schema/DependenciesValidator.java
@@ -22,11 +22,23 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for dependencies.
+ */
public class DependenciesValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(DependenciesValidator.class);
private final Map> propertyDeps = new HashMap>();
private final Map schemaDeps = new HashMap();
+ /**
+ * Constructor.
+ *
+ * @param schemaLocation the schema location
+ * @param evaluationPath the evaluation path
+ * @param schemaNode the schema node
+ * @param parentSchema the parent schema
+ * @param validationContext the validation context
+ */
public DependenciesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES, validationContext);
@@ -61,8 +73,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (deps != null && !deps.isEmpty()) {
for (String field : deps) {
if (node.get(field) == null) {
- errors.add(message().property(pname).instanceLocation(instanceLocation)
+ errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation)
.locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
.arguments(propertyDeps.toString()).build());
}
}
diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java
index 24248be27..dda8ec5ae 100644
--- a/src/main/java/com/networknt/schema/DependentRequired.java
+++ b/src/main/java/com/networknt/schema/DependentRequired.java
@@ -22,6 +22,9 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for dependentRequired.
+ */
public class DependentRequired extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(DependentRequired.class);
private final Map> propertyDependencies = new HashMap>();
@@ -54,8 +57,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (dependencies != null && !dependencies.isEmpty()) {
for (String field : dependencies) {
if (node.get(field) == null) {
- errors.add(message().property(pname).instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(field, pname)
+ errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).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 51ee49eac..5781eb54b 100644
--- a/src/main/java/com/networknt/schema/DependentSchemas.java
+++ b/src/main/java/com/networknt/schema/DependentSchemas.java
@@ -22,6 +22,9 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for dependentSchemas.
+ */
public class DependentSchemas extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(DependentSchemas.class);
private final Map schemaDependencies = new HashMap<>();
diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java
index d2ce7a67a..a113d1167 100644
--- a/src/main/java/com/networknt/schema/DynamicRefValidator.java
+++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java
@@ -17,14 +17,13 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
- * Resolves $dynamicRef.
+ * {@link JsonValidator} that resolves $dynamicRef.
*/
public class DynamicRefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(DynamicRefValidator.class);
@@ -32,7 +31,7 @@ public class DynamicRefValidator extends BaseJsonValidator {
protected JsonSchemaRef schema;
public DynamicRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
- super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext);
+ super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DYNAMIC_REF, validationContext);
String refValue = schemaNode.asText();
this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath);
}
@@ -85,72 +84,42 @@ private static String resolve(JsonSchema parentSchema, String refValue) {
}
return SchemaLocation.resolve(base.getSchemaLocation(), refValue);
}
-
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
- CollectorContext collectorContext = executionContext.getCollectorContext();
-
- Set errors = Collections.emptySet();
-
- Scope parentScope = collectorContext.enterDynamicScope();
- try {
- debug(logger, node, rootNode, instanceLocation);
- JsonSchema refSchema = this.schema.getSchema();
- if (refSchema == null) {
- ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue())
- .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
- .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
- .arguments(schemaNode.asText()).build();
- throw new JsonSchemaException(validationMessage);
- }
- errors = refSchema.validate(executionContext, node, rootNode, instanceLocation);
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
+ debug(logger, node, rootNode, instanceLocation);
+ JsonSchema refSchema = this.schema.getSchema();
+ if (refSchema == null) {
+ ValidationMessage validationMessage = message().type(ValidatorTypeCode.DYNAMIC_REF.getValue())
+ .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
+ .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
+ .arguments(schemaNode.asText()).build();
+ throw new InvalidSchemaRefException(validationMessage);
}
- return errors;
+ return refSchema.validate(executionContext, node, rootNode, instanceLocation);
}
@Override
public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
- CollectorContext collectorContext = executionContext.getCollectorContext();
-
- Set errors = Collections.emptySet();
-
- Scope parentScope = collectorContext.enterDynamicScope();
- try {
- debug(logger, node, rootNode, instanceLocation);
- // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
- // these schemas will be cached along with config. We have to replace the config for cached $ref references
- // with the latest config. Reset the config.
- JsonSchema refSchema = this.schema.getSchema();
- if (refSchema == null) {
- ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue())
- .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
- .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
- .arguments(schemaNode.asText()).build();
- throw new JsonSchemaException(validationMessage);
- }
- errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
- return errors;
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (shouldValidateSchema) {
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
- }
+ debug(logger, node, rootNode, instanceLocation);
+ // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
+ // these schemas will be cached along with config. We have to replace the config for cached $ref references
+ // with the latest config. Reset the config.
+ JsonSchema refSchema = this.schema.getSchema();
+ if (refSchema == null) {
+ ValidationMessage validationMessage = message().type(ValidatorTypeCode.DYNAMIC_REF.getValue())
+ .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
+ .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
+ .arguments(schemaNode.asText()).build();
+ throw new InvalidSchemaRefException(validationMessage);
}
+ return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
}
public JsonSchemaRef getSchemaRef() {
return this.schema;
}
-
@Override
public void preloadJsonSchema() {
JsonSchema jsonSchema = null;
@@ -168,14 +137,14 @@ public void preloadJsonSchema() {
SchemaLocation schemaLocation = jsonSchema.getSchemaLocation();
JsonSchema check = jsonSchema;
boolean circularDependency = false;
- while(check.getEvaluationParentSchema() != null) {
+ while (check.getEvaluationParentSchema() != null) {
check = check.getEvaluationParentSchema();
if (check.getSchemaLocation().equals(schemaLocation)) {
circularDependency = true;
break;
}
}
- if(!circularDependency) {
+ if (!circularDependency) {
jsonSchema.initializeValidators();
}
}
diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java
index eb006b584..1e088c648 100644
--- a/src/main/java/com/networknt/schema/EnumValidator.java
+++ b/src/main/java/com/networknt/schema/EnumValidator.java
@@ -28,6 +28,9 @@
import java.util.HashSet;
import java.util.Set;
+/**
+ * {@link JsonValidator} for enum.
+ */
public class EnumValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(EnumValidator.class);
@@ -87,8 +90,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
node = processArrayNode((ArrayNode) node);
}
if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(error).build());
}
return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
index b67894600..88c1bde82 100644
--- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
+++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
@@ -27,6 +27,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for exclusiveMaximum.
+ */
public class ExclusiveMaximumValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ExclusiveMaximumValidator.class);
@@ -103,9 +106,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
if (typedMaximum.crossesThreshold(node)) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue())
- .build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
+ .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 db0655330..a53a56b76 100644
--- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java
+++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java
@@ -27,6 +27,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for exclusiveMinimum.
+ */
public class ExclusiveMinimumValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ExclusiveMinimumValidator.class);
@@ -110,9 +113,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
if (typedMinimum.crossesThreshold(node)) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue())
- .build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
+ .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 594e3e88d..f1a04569b 100644
--- a/src/main/java/com/networknt/schema/ExecutionConfig.java
+++ b/src/main/java/com/networknt/schema/ExecutionConfig.java
@@ -24,70 +24,53 @@
* Configuration per execution.
*/
public class ExecutionConfig {
+ /**
+ * The locale to use for formatting messages.
+ */
private Locale locale = Locale.ROOT;
- private Predicate annotationAllowedPredicate = (keyword) -> true;
+
+ /**
+ * Determines if annotation collection is enabled.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ */
+ private boolean annotationCollectionEnabled = false;
+
+ /**
+ * If annotation collection is enabled, determine which annotations to collect.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ */
+ private Predicate annotationCollectionPredicate = keyword -> false;
/**
* Since Draft 2019-09 format assertions are not enabled by default.
*/
private Boolean formatAssertionsEnabled = null;
- public Locale getLocale() {
- return locale;
- }
-
- public void setLocale(Locale locale) {
- this.locale = Objects.requireNonNull(locale, "Locale must not be null");
- }
+ /**
+ * Determine if the validation execution can fail fast.
+ */
+ private boolean failFast = false;
/**
- * 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.
+ * Gets the locale to use for formatting messages.
*
- * @return the predicate to determine if annotation collection is allowed for
- * the keyword
+ * @return the locale
*/
- public Predicate getAnnotationAllowedPredicate() {
- return annotationAllowedPredicate;
+ public Locale getLocale() {
+ return locale;
}
/**
- * 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.
+ * Sets the locale to use for formatting messages.
*
- * @param annotationAllowedPredicate the predicate accepting the keyword
+ * @param locale the locale
*/
- public void setAnnotationAllowedPredicate(Predicate annotationAllowedPredicate) {
- this.annotationAllowedPredicate = Objects.requireNonNull(annotationAllowedPredicate,
- "annotationAllowedPredicate must not be null");
+ public void setLocale(Locale locale) {
+ this.locale = Objects.requireNonNull(locale, "Locale must not be null");
}
/**
@@ -113,4 +96,88 @@ public Boolean getFormatAssertionsEnabled() {
public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) {
this.formatAssertionsEnabled = formatAssertionsEnabled;
}
+
+ /**
+ * Return if fast fail is enabled.
+ *
+ * @return if fast fail is enabled
+ */
+ public boolean isFailFast() {
+ return failFast;
+ }
+
+ /**
+ * Sets whether fast fail is enabled.
+ *
+ * @param failFast true to fast fail
+ */
+ public void setFailFast(boolean failFast) {
+ this.failFast = failFast;
+ }
+
+ /**
+ * Return if annotation collection is enabled.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ *
+ * The annotations to collect can be customized using the annotation collection
+ * predicate.
+ *
+ * @return if annotation collection is enabled
+ */
+ protected boolean isAnnotationCollectionEnabled() {
+ return annotationCollectionEnabled;
+ }
+
+ /**
+ * Sets whether to annotation collection is enabled.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ *
+ * The annotations to collect can be customized using the annotation collection
+ * predicate.
+ *
+ * @param annotationCollectionEnabled true to enable annotation collection
+ */
+ protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) {
+ this.annotationCollectionEnabled = annotationCollectionEnabled;
+ }
+
+ /**
+ * Gets the predicate to determine if annotation collection is allowed for a
+ * particular keyword. This only has an effect if annotation collection is
+ * enabled.
+ *
+ * The default value is to not collect any annotation keywords if annotation
+ * collection is enabled.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ *
+ * @return the predicate to determine if annotation collection is allowed for
+ * the keyword
+ */
+ public Predicate getAnnotationCollectionPredicate() {
+ return annotationCollectionPredicate;
+ }
+
+ /**
+ * Predicate to determine if annotation collection is allowed for a particular
+ * keyword. This only has an effect if annotation collection is enabled.
+ *
+ * The default value is to not collect any annotation keywords if annotation
+ * collection is enabled.
+ *
+ * This does not affect annotation collection required for evaluating keywords
+ * such as unevaluatedItems or unevaluatedProperties and only affects reporting.
+ *
+ * @param annotationCollectionPredicate the predicate accepting the keyword
+ */
+ public void setAnnotationCollectionPredicate(Predicate annotationCollectionPredicate) {
+ this.annotationCollectionPredicate = Objects.requireNonNull(annotationCollectionPredicate,
+ "annotationCollectionPredicate 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 c3129eb03..05becdd23 100644
--- a/src/main/java/com/networknt/schema/ExecutionContext.java
+++ b/src/main/java/com/networknt/schema/ExecutionContext.java
@@ -16,6 +16,9 @@
package com.networknt.schema;
+import com.networknt.schema.annotation.JsonNodeAnnotations;
+import com.networknt.schema.result.JsonNodeResults;
+
import java.util.Stack;
/**
@@ -26,6 +29,8 @@ public class ExecutionContext {
private CollectorContext collectorContext;
private ValidatorState validatorState = null;
private Stack discriminatorContexts = new Stack<>();
+ private JsonNodeAnnotations annotations = new JsonNodeAnnotations();
+ private JsonNodeResults results = new JsonNodeResults();
/**
* Creates an execution context.
@@ -99,6 +104,14 @@ public void setExecutionConfig(ExecutionConfig executionConfig) {
this.executionConfig = executionConfig;
}
+ public JsonNodeAnnotations getAnnotations() {
+ return annotations;
+ }
+
+ public JsonNodeResults getResults() {
+ return results;
+ }
+
/**
* Gets the validator state.
*
diff --git a/src/main/java/com/networknt/schema/FailFastAssertionException.java b/src/main/java/com/networknt/schema/FailFastAssertionException.java
new file mode 100644
index 000000000..6ea3cf455
--- /dev/null
+++ b/src/main/java/com/networknt/schema/FailFastAssertionException.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024 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.Objects;
+import java.util.Set;
+
+/**
+ * Thrown when an assertion happens and the evaluation can fail fast.
+ *
+ * This doesn't extend off JsonSchemaException as it is used for flow control
+ * and is intended to be caught in a specific place.
+ *
+ * This will be caught in the JsonSchema validate method to be passed to the
+ * output formatter.
+ */
+public class FailFastAssertionException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ private final ValidationMessage validationMessage;
+
+ /**
+ * Constructor.
+ *
+ * @param validationMessage the validation message
+ */
+ public FailFastAssertionException(ValidationMessage validationMessage) {
+ this.validationMessage = Objects.requireNonNull(validationMessage);
+ }
+
+ /**
+ * Gets the validation message.
+ *
+ * @return the validation message
+ */
+ public ValidationMessage getValidationMessage() {
+ return this.validationMessage;
+ }
+
+ /**
+ * Gets the validation message.
+ *
+ * @return the validation message
+ */
+ public Set getValidationMessages() {
+ return Collections.singleton(this.validationMessage);
+ }
+
+ @Override
+ public String getMessage() {
+ return this.validationMessage != null ? this.validationMessage.getMessage() : super.getMessage();
+ }
+
+ @Override
+ public Throwable fillInStackTrace() {
+ /*
+ * This is overridden for performance as filling in the stack trace is expensive
+ * and this is used for flow control.
+ */
+ return this;
+ }
+}
diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java
index 675b4a601..3fa2ba4b2 100644
--- a/src/main/java/com/networknt/schema/FalseValidator.java
+++ b/src/main/java/com/networknt/schema/FalseValidator.java
@@ -22,6 +22,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for false.
+ */
public class FalseValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(FalseValidator.class);
@@ -32,7 +35,8 @@ public FalseValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath
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(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).build());
}
}
diff --git a/src/main/java/com/networknt/schema/Format.java b/src/main/java/com/networknt/schema/Format.java
index 3120cbcdf..18f80b813 100644
--- a/src/main/java/com/networknt/schema/Format.java
+++ b/src/main/java/com/networknt/schema/Format.java
@@ -16,12 +16,22 @@
package com.networknt.schema;
+/**
+ * Used to implement the various formats for the format keyword.
+ */
public interface Format {
/**
* @return the format name as referred to in a json schema format node.
*/
String getName();
+ /**
+ * Determines if the value matches the format.
+ *
+ * @param executionContext the execution context
+ * @param value to match
+ * @return true if matches
+ */
boolean matches(ExecutionContext executionContext, String value);
String getErrorMessageDescription();
diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java
index cb3cb1f58..9fa21b313 100644
--- a/src/main/java/com/networknt/schema/FormatValidator.java
+++ b/src/main/java/com/networknt/schema/FormatValidator.java
@@ -40,6 +40,13 @@ public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPat
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
debug(logger, node, rootNode, instanceLocation);
+ if (format != null) {
+ if (collectAnnotations(executionContext)) {
+ putAnnotation(executionContext,
+ annotation -> annotation.instanceLocation(instanceLocation).value(this.format.getName()));
+ }
+ }
+
JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig());
if (nodeType != JsonType.STRING) {
return Collections.emptySet();
@@ -52,14 +59,15 @@ public Set validate(ExecutionContext executionContext, JsonNo
if(!node.textValue().trim().equals(node.textValue())) {
if (assertionsEnabled) {
// leading and trailing spaces
- errors.add(message().instanceLocation(instanceLocation)
+ errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
.locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
.arguments(format.getName(), format.getErrorMessageDescription()).build());
}
} else if(node.textValue().contains("%")) {
if (assertionsEnabled) {
// zone id is not part of the ipv6
- errors.add(message().instanceLocation(instanceLocation)
+ errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
.locale(executionContext.getExecutionConfig().getLocale())
.arguments(format.getName(), format.getErrorMessageDescription()).build());
}
@@ -68,7 +76,7 @@ public Set validate(ExecutionContext executionContext, JsonNo
try {
if (!format.matches(executionContext, node.textValue())) {
if (assertionsEnabled) {
- errors.add(message().instanceLocation(instanceLocation)
+ errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
.locale(executionContext.getExecutionConfig().getLocale())
.arguments(format.getName(), format.getErrorMessageDescription()).build());
}
diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java
index 0d90912ff..f6bacb795 100644
--- a/src/main/java/com/networknt/schema/IfValidator.java
+++ b/src/main/java/com/networknt/schema/IfValidator.java
@@ -17,13 +17,15 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} for if.
+ */
public class IfValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(IfValidator.class);
@@ -64,38 +66,25 @@ public IfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, J
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
debug(logger, node, rootNode, instanceLocation);
- CollectorContext collectorContext = executionContext.getCollectorContext();
-
Set errors = new LinkedHashSet<>();
- Scope parentScope = collectorContext.enterDynamicScope();
boolean ifConditionPassed = false;
- try {
- try {
- 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
- ifConditionPassed = false;
- }
-
- if (ifConditionPassed && this.thenSchema != null) {
- 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, instanceLocation));
- }
+ // Save flag as nested schema evaluation shouldn't trigger fail fast
+ boolean failFast = executionContext.getExecutionConfig().isFailFast();
+ try {
+ executionContext.getExecutionConfig().setFailFast(false);
+ ifConditionPassed = this.ifSchema.validate(executionContext, node, rootNode, instanceLocation).isEmpty();
} finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
+ // Restore flag
+ executionContext.getExecutionConfig().setFailFast(failFast);
}
+ if (ifConditionPassed && this.thenSchema != null) {
+ errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, instanceLocation));
+ } else if (!ifConditionPassed && this.elseSchema != null) {
+ errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, instanceLocation));
+ }
return Collections.unmodifiableSet(errors);
}
diff --git a/src/main/java/com/networknt/schema/InvalidSchemaException.java b/src/main/java/com/networknt/schema/InvalidSchemaException.java
new file mode 100644
index 000000000..e60a34811
--- /dev/null
+++ b/src/main/java/com/networknt/schema/InvalidSchemaException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2024 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;
+
+/**
+ * Thrown when an invalid schema is used.
+ */
+public class InvalidSchemaException extends JsonSchemaException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidSchemaException(ValidationMessage message, Exception cause) {
+ super(Objects.requireNonNull(message));
+ this.initCause(cause);
+ }
+
+ public InvalidSchemaException(ValidationMessage message) {
+ super(Objects.requireNonNull(message));
+ }
+}
diff --git a/src/main/java/com/networknt/schema/InvalidSchemaRefException.java b/src/main/java/com/networknt/schema/InvalidSchemaRefException.java
new file mode 100644
index 000000000..9aad3a5c6
--- /dev/null
+++ b/src/main/java/com/networknt/schema/InvalidSchemaRefException.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2024 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;
+
+/**
+ * Thrown when an invalid schema ref is used.
+ */
+public class InvalidSchemaRefException extends InvalidSchemaException {
+ private static final long serialVersionUID = 1L;
+
+ public InvalidSchemaRefException(ValidationMessage message, Exception cause) {
+ super(message, cause);
+ }
+
+ public InvalidSchemaRefException(ValidationMessage message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 822406486..0ba438f11 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -18,6 +18,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.networknt.schema.annotation.JsonNodeAnnotation;
import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
@@ -26,6 +27,9 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for items V4 to V2019-09.
+ */
public class ItemsValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ItemsValidator.class);
private static final String PROPERTY_ADDITIONAL_ITEMS = "additionalItems";
@@ -36,6 +40,8 @@ public class ItemsValidator extends BaseJsonValidator {
private final JsonSchema additionalSchema;
private WalkListenerRunner arrayItemWalkListenerRunner;
+ private Boolean hasUnevaluatedItemsValidator = null;
+
public ItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext);
@@ -78,72 +84,106 @@ public Set validate(ExecutionContext executionContext, JsonNo
// ignores non-arrays
return Collections.emptySet();
}
+ boolean collectAnnotations = collectAnnotations();
+
+ // Add items annotation
+ if (collectAnnotations || collectAnnotations(executionContext)) {
+ if (this.schema != null) {
+ // Applies to all
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(true).build());
+ } else if (this.tupleSchema != null) {
+ // Tuples
+ int items = node.isArray() ? node.size() : 1;
+ int schemas = this.tupleSchema.size();
+ if (items > schemas) {
+ // More items than schemas so the keyword only applied to the number of schemas
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(schemas).build());
+ } else {
+ // Applies to all
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(true).build());
+ }
+ }
+ }
+
+ boolean hasAdditionalItem = false;
Set errors = new LinkedHashSet<>();
if (node.isArray()) {
int i = 0;
for (JsonNode n : node) {
- doValidate(executionContext, errors, i, n, rootNode, instanceLocation);
+ if (doValidate(executionContext, errors, i, n, rootNode, instanceLocation)) {
+ hasAdditionalItem = true;
+ }
i++;
}
} else {
- doValidate(executionContext, errors, 0, node, rootNode, instanceLocation);
+ if (doValidate(executionContext, errors, 0, node, rootNode, instanceLocation)) {
+ hasAdditionalItem = true;
+ }
+ }
+
+ if (hasAdditionalItem) {
+ if (collectAnnotations || collectAnnotations(executionContext, "additionalItems")) {
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword("additionalItems").value(true).build());
+ }
}
return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors);
}
- private void doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node,
+ private boolean doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node,
JsonNode rootNode, JsonNodePath instanceLocation) {
- Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems();
+ boolean isAdditionalItem = false;
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()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
- } else {
+ if (!results.isEmpty()) {
errors.addAll(results);
}
} else if (this.tupleSchema != null) {
if (i < this.tupleSchema.size()) {
// validate against tuple schema
Set results = this.tupleSchema.get(i).validate(executionContext, node, rootNode, path);
- if (results.isEmpty()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
- } else {
+ if (!results.isEmpty()) {
errors.addAll(results);
}
} else {
+ if ((this.additionalItems != null && this.additionalItems) || this.additionalSchema != null) {
+ isAdditionalItem = true;
+ }
+
if (this.additionalSchema != null) {
// validate against additional item schema
Set results = this.additionalSchema.validate(executionContext, node, rootNode, path);
- if (results.isEmpty()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
- } else {
+ if (!results.isEmpty()) {
errors.addAll(results);
}
} else if (this.additionalItems != null) {
if (this.additionalItems) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
+// evaluatedItems.add(path);
} else {
// no additional item allowed, return error
- errors.add(message().instanceLocation(path)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(i).build());
+ errors.add(message().instanceNode(node).instanceLocation(path)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(i).build());
}
}
}
-// } else {
-// evaluatedItems.add(path);
}
+ return isAdditionalItem;
}
@Override
@@ -213,6 +253,17 @@ public List getTupleSchema() {
public JsonSchema getSchema() {
return this.schema;
}
+
+ private boolean collectAnnotations() {
+ return hasUnevaluatedItemsValidator();
+ }
+
+ private boolean hasUnevaluatedItemsValidator() {
+ if (this.hasUnevaluatedItemsValidator == null) {
+ this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems");
+ }
+ return hasUnevaluatedItemsValidator;
+ }
@Override
public void preloadJsonSchema() {
@@ -223,5 +274,6 @@ public void preloadJsonSchema() {
if (null != this.additionalSchema) {
this.additionalSchema.initializeValidators();
}
+ collectAnnotations(); // cache the flag
}
}
diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java
index 6f770a4ff..459867c83 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator202012.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java
@@ -18,6 +18,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.networknt.schema.annotation.JsonNodeAnnotation;
import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
@@ -26,6 +27,9 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for items from V2012-12.
+ */
public class ItemsValidator202012 extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ItemsValidator202012.class);
@@ -33,6 +37,8 @@ public class ItemsValidator202012 extends BaseJsonValidator {
private final WalkListenerRunner arrayItemWalkListenerRunner;
private final int prefixCount;
+ private Boolean hasUnevaluatedItemsValidator = null;
+
public ItemsValidator202012(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS_202012, validationContext);
@@ -61,18 +67,27 @@ public Set validate(ExecutionContext executionContext, JsonNo
// ignores non-arrays
if (node.isArray()) {
Set errors = new LinkedHashSet<>();
- Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems();
+// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems();
+ boolean evaluated = false;
for (int i = this.prefixCount; i < node.size(); ++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()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
+// evaluatedItems.add(path);
} else {
errors.addAll(results);
}
+ evaluated = true;
+ }
+ if (evaluated) {
+ if (collectAnnotations() || collectAnnotations(executionContext)) {
+ // Applies to all
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(true).build());
+ }
}
return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors);
} else {
@@ -145,6 +160,18 @@ public JsonSchema getSchema() {
@Override
public void preloadJsonSchema() {
this.schema.initializeValidators();
+ collectAnnotations(); // cache the flag
+ }
+
+ private boolean collectAnnotations() {
+ return hasUnevaluatedItemsValidator();
+ }
+
+ private boolean hasUnevaluatedItemsValidator() {
+ if (this.hasUnevaluatedItemsValidator == null) {
+ this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems");
+ }
+ return hasUnevaluatedItemsValidator;
}
}
diff --git a/src/main/java/com/networknt/schema/JsonNodePath.java b/src/main/java/com/networknt/schema/JsonNodePath.java
index d7210be90..55001859b 100644
--- a/src/main/java/com/networknt/schema/JsonNodePath.java
+++ b/src/main/java/com/networknt/schema/JsonNodePath.java
@@ -175,6 +175,29 @@ public boolean startsWith(JsonNodePath other) {
}
}
+ /**
+ * Tests if this path contains a string segment that is an exact match.
+ *
+ * This will not match if the segment is a number.
+ *
+ * @param segment the segment to test
+ * @return true if the string segment is found
+ */
+ public boolean contains(String segment) {
+ boolean result = segment.equals(this.pathSegment);
+ if (result) {
+ return true;
+ }
+ JsonNodePath path = this.getParent();
+ while (path != null) {
+ if (segment.equals(path.pathSegment)) {
+ return true;
+ }
+ path = path.getParent();
+ }
+ return false;
+ }
+
@Override
public String toString() {
if (this.value == null) {
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 435b07027..acada7066 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -19,7 +19,6 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.networknt.schema.CollectorContext.Scope;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.serialization.JsonMapperFactory;
import com.networknt.schema.serialization.YamlMapperFactory;
@@ -62,30 +61,66 @@ static JsonSchema from(ValidationContext validationContext, SchemaLocation schem
private boolean hasNoFragment(SchemaLocation schemaLocation) {
return this.schemaLocation.getFragment() == null || this.schemaLocation.getFragment().getNameCount() == 0;
}
+
+ private static SchemaLocation resolve(SchemaLocation schemaLocation, JsonNode schemaNode, boolean rootSchema,
+ ValidationContext validationContext) {
+ String id = validationContext.resolveSchemaId(schemaNode);
+ if (id != null) {
+ String resolve = id;
+ int fragment = id.indexOf('#');
+ // Check if there is a non-empty fragment
+ if (fragment != -1 && !(fragment + 1 >= id.length())) {
+ // strip the fragment when resolving
+ resolve = id.substring(0, fragment);
+ }
+ SchemaLocation result = !"".equals(resolve) ? schemaLocation.resolve(resolve) : schemaLocation;
+ JsonSchemaIdValidator validator = validationContext.getConfig().getSchemaIdValidator();
+ if (validator != null) {
+ if (!validator.validate(id, rootSchema, schemaLocation, result, validationContext)) {
+ SchemaLocation idSchemaLocation = schemaLocation.append(validationContext.getMetaSchema().getIdKeyword());
+ ValidationMessage validationMessage = ValidationMessage.builder()
+ .code(ValidatorTypeCode.ID.getValue()).type(ValidatorTypeCode.ID.getValue())
+ .instanceLocation(idSchemaLocation.getFragment())
+ .arguments(schemaLocation.toString(), id)
+ .schemaLocation(idSchemaLocation)
+ .schemaNode(schemaNode)
+ .messageFormatter(args -> validationContext.getConfig().getMessageSource().getMessage(
+ ValidatorTypeCode.ID.getValue(), validationContext.getConfig().getLocale(), args))
+ .build();
+ throw new InvalidSchemaException(validationMessage);
+ }
+ }
+ return result;
+ } else {
+ return schemaLocation;
+ }
+ }
private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath,
JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) {
- super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent,
+ super(resolve(schemaLocation, schemaNode, parent == null, validationContext), evaluationPath, schemaNode, parent,
null, null, validationContext, suppressSubSchemaRetrieval);
this.metaSchema = this.validationContext.getMetaSchema();
initializeConfig();
String id = this.validationContext.resolveSchemaId(this.schemaNode);
if (id != null) {
- // In earlier drafts $id may contain an anchor fragment
+ // In earlier drafts $id may contain an anchor fragment see draft4/idRef.json
// Note that json pointer fragments in $id are not allowed
- if (hasNoFragment(schemaLocation)) {
+ SchemaLocation result = id.contains("#") ? schemaLocation.resolve(id) : this.schemaLocation;
+ if (hasNoFragment(result)) {
this.id = id;
} else {
- this.id = id;
+ // This is an anchor fragment and is not a document
+ // This will be added to schema resources later
+ this.id = null;
}
- this.validationContext.getSchemaResources()
- .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : id, this);
+ this.validationContext.getSchemaResources().putIfAbsent(result != null ? result.toString() : id, this);
} else {
if (hasNoFragment(schemaLocation)) {
// No $id but there is no fragment and is thus a schema resource
- this.id = this.schemaLocation.getAbsoluteIri() != null ? this.schemaLocation.getAbsoluteIri().toString() : "";
+ this.id = schemaLocation.getAbsoluteIri() != null ? schemaLocation.getAbsoluteIri().toString() : "";
this.validationContext.getSchemaResources()
- .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this);
+ .putIfAbsent(schemaLocation != null ? schemaLocation.toString() : this.id, this);
} else {
this.id = null;
}
@@ -248,6 +283,10 @@ public JsonSchema getSubSchema(JsonNodePath fragment) {
JsonSchema document = findSchemaResourceRoot();
JsonSchema parent = document;
JsonSchema subSchema = null;
+ JsonNode parentNode = parent.getSchemaNode();
+ SchemaLocation schemaLocation = document.getSchemaLocation();
+ JsonNodePath evaluationPath = document.getEvaluationPath();
+ int nameCount = fragment.getNameCount();
for (int x = 0; x < fragment.getNameCount(); x++) {
/*
* The sub schema is created by iterating through the parents in order to
@@ -258,10 +297,8 @@ public JsonSchema getSubSchema(JsonNodePath fragment) {
* to $id changes in the lexical scope.
*/
Object segment = fragment.getElement(x);
- JsonNode subSchemaNode = parent.getNode(segment);
+ JsonNode subSchemaNode = getNode(parentNode, segment);
if (subSchemaNode != null) {
- SchemaLocation schemaLocation = parent.getSchemaLocation();
- JsonNodePath evaluationPath = parent.getEvaluationPath();
if (segment instanceof Number) {
int index = ((Number) segment).intValue();
schemaLocation = schemaLocation.append(index);
@@ -274,9 +311,17 @@ public JsonSchema getSubSchema(JsonNodePath fragment) {
* The parent validation context is used to create as there can be changes in
* $schema is later drafts which means the validation context can change.
*/
- subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode,
- parent);
- parent = subSchema;
+ // This may need a redesign see Issue 939 and 940
+ String id = parent.getValidationContext().resolveSchemaId(subSchemaNode);
+// if (!("definitions".equals(segment.toString()) || "$defs".equals(segment.toString())
+// )) {
+ if (id != null || x == nameCount - 1) {
+ subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode,
+ parent);
+ parent = subSchema;
+ schemaLocation = subSchema.getSchemaLocation();
+ }
+ parentNode = subSchemaNode;
} else {
/*
* This means that the fragment wasn't found in the document.
@@ -290,9 +335,14 @@ public JsonSchema getSubSchema(JsonNodePath fragment) {
found = found.getSubSchema(fragment);
}
if (found == null) {
- throw new JsonSchemaException("Unable to find subschema " + fragment.toString() + " in "
- + parent.getSchemaLocation().toString() + " at evaluation path "
- + parent.getEvaluationPath().toString());
+ ValidationMessage validationMessage = ValidationMessage.builder()
+ .type(ValidatorTypeCode.REF.getValue()).code("internal.unresolvedRef")
+ .message("{0}: Reference {1} cannot be resolved")
+ .instanceLocation(schemaLocation.getFragment())
+ .schemaLocation(schemaLocation)
+ .evaluationPath(evaluationPath)
+ .arguments(fragment).build();
+ throw new InvalidSchemaRefException(validationMessage);
}
return found;
}
@@ -301,7 +351,10 @@ public JsonSchema getSubSchema(JsonNodePath fragment) {
}
protected JsonNode getNode(Object propertyOrIndex) {
- JsonNode node = getSchemaNode();
+ return getNode(this.schemaNode, propertyOrIndex);
+ }
+
+ protected JsonNode getNode(JsonNode node, Object propertyOrIndex) {
JsonNode value = null;
if (propertyOrIndex instanceof Number) {
value = node.get(((Number) propertyOrIndex).intValue());
@@ -506,36 +559,24 @@ public Set validate(ExecutionContext executionContext, JsonNo
SchemaValidatorsConfig config = this.validationContext.getConfig();
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()) {
Set results = null;
- Scope parentScope = collectorContext.enterDynamicScope(this);
try {
results = v.validate(executionContext, jsonNode, rootNode, instanceLocation);
} finally {
- Scope scope = collectorContext.exitDynamicScope();
if (results == null || results.isEmpty()) {
- parentScope.mergeWith(scope);
+ // Do nothing if valid
} else {
+ executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), false);
if (errors == null) {
errors = new LinkedHashSet<>();
}
errors.addAll(results);
- if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator
- || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) {
- collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems());
- }
- if (v instanceof PropertiesValidator || v instanceof AdditionalPropertiesValidator
- || v instanceof PatternPropertiesValidator) {
- collectorContext.getEvaluatedProperties().addAll(scope.getEvaluatedProperties());
- }
}
-
}
}
@@ -804,6 +845,44 @@ public T validate(String input, InputFormat inputFormat, OutputFormat for
});
}
+ /**
+ * 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,
+ ExecutionContextCustomizer executionCustomizer) {
+ format.customize(executionContext, this.validationContext);
+ if (executionCustomizer != null) {
+ executionCustomizer.customize(executionContext, this.validationContext);
+ }
+ Set validationMessages = null;
+ try {
+ validationMessages = validate(executionContext, node);
+ } catch (FailFastAssertionException e) {
+ validationMessages = e.getValidationMessages();
+ }
+ return format.format(this, validationMessages, executionContext, this.validationContext);
+ }
+
/**
* Deserialize string to JsonNode.
*
@@ -956,7 +1035,6 @@ private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, J
public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
JsonNodePath instanceLocation, boolean shouldValidateSchema) {
Set errors = new LinkedHashSet<>();
- CollectorContext collectorContext = executionContext.getCollectorContext();
// Walk through all the JSONWalker's.
for (JsonValidator v : getValidators()) {
JsonNodePath evaluationPathWithKeyword = v.getEvaluationPath();
@@ -968,23 +1046,12 @@ public Set walk(ExecutionContext executionContext, JsonNode n
v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode,
this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory())) {
Set results = null;
- Scope parentScope = collectorContext.enterDynamicScope(this);
try {
results = v.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
} finally {
- Scope scope = collectorContext.exitDynamicScope();
if (results == null || results.isEmpty()) {
- parentScope.mergeWith(scope);
} else {
errors.addAll(results);
- if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator
- || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) {
- collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems());
- }
- if (v instanceof PropertiesValidator || v instanceof AdditionalPropertiesValidator
- || v instanceof PatternPropertiesValidator) {
- collectorContext.getEvaluatedProperties().addAll(scope.getEvaluatedProperties());
- }
}
}
}
@@ -1079,13 +1146,13 @@ public boolean isRecursiveAnchor() {
*/
public ExecutionContext createExecutionContext() {
SchemaValidatorsConfig config = validationContext.getConfig();
- CollectorContext collectorContext = new CollectorContext(config.isUnevaluatedItemsAnalysisDisabled(),
- config.isUnevaluatedPropertiesAnalysisDisabled());
+ CollectorContext collectorContext = new CollectorContext();
// Copy execution config defaults from validation config
ExecutionConfig executionConfig = new ExecutionConfig();
executionConfig.setLocale(config.getLocale());
executionConfig.setFormatAssertionsEnabled(config.getFormatAssertionsEnabled());
+ executionConfig.setFailFast(config.isFailFast());
ExecutionContext executionContext = new ExecutionContext(executionConfig, collectorContext);
if(config.getExecutionContextCustomizer() != null) {
diff --git a/src/main/java/com/networknt/schema/JsonSchemaException.java b/src/main/java/com/networknt/schema/JsonSchemaException.java
index a95d1463e..2113d2e47 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaException.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaException.java
@@ -24,7 +24,6 @@ public class JsonSchemaException extends RuntimeException {
private ValidationMessage validationMessage;
public JsonSchemaException(ValidationMessage validationMessage) {
- super(validationMessage.getMessage());
this.validationMessage = validationMessage;
}
@@ -36,6 +35,15 @@ public JsonSchemaException(Throwable throwable) {
super(throwable);
}
+ @Override
+ public String getMessage() {
+ return this.validationMessage != null ? this.validationMessage.getMessage() : super.getMessage();
+ }
+
+ public ValidationMessage getValidationMessage() {
+ return this.validationMessage;
+ }
+
public Set getValidationMessages() {
if (validationMessage == null) {
return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index f68d1d4e5..596332c7b 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -255,7 +255,7 @@ public static Builder builder(final JsonSchemaFactory blueprint) {
*/
protected JsonSchema newJsonSchema(final SchemaLocation schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) {
final ValidationContext validationContext = createValidationContext(schemaNode, config);
- JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext),
+ JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri),
new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false);
try {
/*
@@ -306,20 +306,17 @@ private ValidationContext withMetaSchema(ValidationContext validationContext, Js
}
/**
- * Gets the schema location from the $id or retrieval uri.
+ * Gets the base IRI from the schema retrieval IRI if present otherwise return
+ * one with a null base IRI.
+ *
+ * Note that the resolving of the $id or id in the schema node will take place
+ * in the JsonSchema constructor.
*
- * @param schemaRetrievalUri the schema retrieval uri
- * @param schemaNode the schema json
- * @param validationContext the validationContext
+ * @param schemaLocation the schema retrieval uri
* @return the schema location
*/
- protected SchemaLocation getSchemaLocation(SchemaLocation 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 SchemaLocation getSchemaLocation(SchemaLocation schemaLocation) {
+ return schemaLocation != null ? schemaLocation : SchemaLocation.DOCUMENT;
}
protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) {
@@ -356,23 +353,29 @@ public JsonMetaSchema getMetaSchema(String id, SchemaValidatorsConfig config) {
}
protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config) {
- JsonSchema schema = getSchema(SchemaLocation.of(id), config);
- JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema());
- VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification();
- if (specification != null) {
- if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) {
- // Process vocabularies
- JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary");
- if (vocabulary != null) {
- builder.vocabularies(new HashMap<>());
- for(Entry vocabs : vocabulary.properties()) {
- builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue());
+ try {
+ JsonSchema schema = getSchema(SchemaLocation.of(id), config);
+ JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema());
+ VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification();
+ if (specification != null) {
+ if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) {
+ // Process vocabularies
+ JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary");
+ if (vocabulary != null) {
+ builder.vocabularies(new HashMap<>());
+ for(Entry vocabs : vocabulary.properties()) {
+ builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue());
+ }
}
+
}
-
}
+ return builder.build();
+ } catch (Exception e) {
+ ValidationMessage validationMessage = ValidationMessage.builder().message("Unknown MetaSchema: {1}")
+ .arguments(id).build();
+ throw new InvalidSchemaException(validationMessage, e);
}
- return builder.build();
}
/**
@@ -450,9 +453,21 @@ public JsonSchema getSchema(final InputStream schemaStream) {
*/
public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) {
if (enableUriSchemaCache) {
- JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(schemaUri, key -> {
- return getMappedSchema(schemaUri, config);
- });
+ // ConcurrentHashMap computeIfAbsent does not allow calls that result in a
+ // recursive update to the map.
+ // The getMapperSchema potentially recurses to call back to getSchema again
+ JsonSchema cachedUriSchema = uriSchemaCache.get(schemaUri);
+ if (cachedUriSchema == null) {
+ synchronized (this) { // acquire lock on shared factory object to prevent deadlock
+ cachedUriSchema = uriSchemaCache.get(schemaUri);
+ if (cachedUriSchema == null) {
+ cachedUriSchema = getMappedSchema(schemaUri, config);
+ if (cachedUriSchema != null) {
+ uriSchemaCache.put(schemaUri, cachedUriSchema);
+ }
+ }
+ }
+ }
return cachedUriSchema.withConfig(config);
}
return getMappedSchema(schemaUri, config);
diff --git a/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java b/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java
new file mode 100644
index 000000000..0629e43f4
--- /dev/null
+++ b/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2024 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.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Validator for validating the correctness of $id.
+ */
+public interface JsonSchemaIdValidator {
+ /**
+ * Validates if the $id value is valid.
+ *
+ * @param id the $id or id
+ * @param rootSchema true if this is a root schema
+ * @param schemaLocation the schema location
+ * @param resolvedSchemaLocation the schema location after resolving with the id
+ * @param validationContext the validation context for instance to get the
+ * meta schema
+ * @return true if valid
+ */
+ boolean validate(String id, boolean rootSchema, SchemaLocation schemaLocation,
+ SchemaLocation resolvedSchemaLocation, ValidationContext validationContext);
+
+ public static final JsonSchemaIdValidator DEFAULT = new DefaultJsonSchemaIdValidator();
+
+ /**
+ * Implementation of {@link JsonSchemaIdValidator}.
+ *
+ * Note that this does not strictly follow the specification.
+ *
+ * This allows an $id that isn't an absolute-IRI on the root schema but it must
+ * resolve to an absolute-IRI given a base-IRI.
+ *
+ * This also allows non-empty fragments.
+ */
+ public static class DefaultJsonSchemaIdValidator implements JsonSchemaIdValidator {
+ @Override
+ public boolean validate(String id, boolean rootSchema, SchemaLocation schemaLocation,
+ SchemaLocation resolvedSchemaLocation, ValidationContext validationContext) {
+ if (hasNoContext(schemaLocation)) {
+ // The following are non-standard
+ if (isFragment(id) || startsWithSlash(id)) {
+ return true;
+ }
+ }
+ return resolvedSchemaLocation.getAbsoluteIri() != null
+ && isAbsoluteIri(resolvedSchemaLocation.getAbsoluteIri().toString());
+ }
+
+ protected boolean startsWithSlash(String id) {
+ return id.startsWith("/");
+ }
+
+ protected boolean isFragment(String id) {
+ return id.startsWith("#");
+ }
+
+ protected boolean hasNoContext(SchemaLocation schemaLocation) {
+ return schemaLocation.getAbsoluteIri() == null || schemaLocation.toString().startsWith("#");
+ }
+
+ protected boolean isAbsoluteIri(String iri) {
+ if (!iri.contains(":")) {
+ return false; // quick check
+ }
+ try {
+ new URI(iri);
+ } catch (URISyntaxException e) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java
index bf9b4c45a..592c80384 100644
--- a/src/main/java/com/networknt/schema/Keyword.java
+++ b/src/main/java/com/networknt/schema/Keyword.java
@@ -18,9 +18,29 @@
import com.fasterxml.jackson.databind.JsonNode;
+/**
+ * Represents a keyword.
+ */
public interface Keyword {
+ /**
+ * Gets the keyword value.
+ *
+ * @return the keyword value
+ */
String getValue();
+ /**
+ * Creates a new validator for the keyword.
+ *
+ * @param schemaLocation the schema location
+ * @param evaluationPath the evaluation path
+ * @param schemaNode the schema node
+ * @param parentSchema the parent schema
+ * @param validationContext the validation context
+ * @return the validation
+ * @throws JsonSchemaException the exception
+ * @throws Exception the exception
+ */
JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception;
}
diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java
index 63f6ccca5..a232349cf 100644
--- a/src/main/java/com/networknt/schema/MaxItemsValidator.java
+++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java
@@ -23,11 +23,13 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for maxItems.
+ */
public class MaxItemsValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MaxItemsValidator.class);
-
private int max = 0;
public MaxItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
@@ -42,11 +44,15 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (node.isArray()) {
if (node.size() > max) {
- return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build());
}
} else if (this.validationContext.getConfig().isTypeLoose()) {
if (1 > max) {
- return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build());
}
}
diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java
index b93be3137..07e7b79d0 100644
--- a/src/main/java/com/networknt/schema/MaxLengthValidator.java
+++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java
@@ -23,6 +23,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for maxLength.
+ */
public class MaxLengthValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MaxLengthValidator.class);
@@ -45,8 +48,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
return Collections.emptySet();
}
if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(maxLength).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(maxLength).build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java
index 56eadd1f1..b50b6c57d 100644
--- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java
@@ -23,6 +23,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator}for maxProperties.
+ */
public class MaxPropertiesValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MaxPropertiesValidator.class);
@@ -41,8 +44,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (node.isObject()) {
if (node.size() > max) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build());
}
}
diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java
index 3bc086073..0137f3b36 100644
--- a/src/main/java/com/networknt/schema/MaximumValidator.java
+++ b/src/main/java/com/networknt/schema/MaximumValidator.java
@@ -27,6 +27,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for maxmimum.
+ */
public class MaximumValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MaximumValidator.class);
private static final String PROPERTY_EXCLUSIVE_MAXIMUM = "exclusiveMaximum";
@@ -113,9 +116,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
if (typedMaximum.crossesThreshold(node)) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue())
- .build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
+ .arguments(typedMaximum.thresholdValue()).build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java
index 69555763f..2257deb0a 100644
--- a/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java
+++ b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java
@@ -17,20 +17,20 @@
import java.util.Locale;
import java.util.Map;
-import java.util.function.Consumer;
+import java.util.function.BiConsumer;
import com.networknt.schema.i18n.MessageSource;
public class MessageSourceValidationMessage {
public static Builder builder(MessageSource messageSource, Map errorMessage,
- Consumer observer) {
+ BiConsumer observer) {
return new Builder(messageSource, errorMessage, observer);
}
public static class Builder extends BuilderSupport {
public Builder(MessageSource messageSource, Map errorMessage,
- Consumer observer) {
+ BiConsumer observer) {
super(messageSource, errorMessage, observer);
}
@@ -41,13 +41,14 @@ public Builder self() {
}
public abstract static class BuilderSupport extends ValidationMessage.BuilderSupport {
- private final Consumer observer;
+ private final BiConsumer observer;
private final MessageSource messageSource;
private final Map errorMessage;
+ private boolean failFast;
private Locale locale;
public BuilderSupport(MessageSource messageSource, Map errorMessage,
- Consumer observer) {
+ BiConsumer observer) {
this.messageSource = messageSource;
this.observer = observer;
this.errorMessage = errorMessage;
@@ -75,7 +76,7 @@ public ValidationMessage build() {
}
ValidationMessage validationMessage = super.build();
if (this.observer != null) {
- this.observer.accept(validationMessage);
+ this.observer.accept(validationMessage, this.failFast);
}
return validationMessage;
}
@@ -84,5 +85,10 @@ public S locale(Locale locale) {
this.locale = locale;
return self();
}
+
+ public S failFast(boolean failFast) {
+ this.failFast = failFast;
+ return self();
+ }
}
}
diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java
index 969b399cb..85e833db8 100644
--- a/src/main/java/com/networknt/schema/MinItemsValidator.java
+++ b/src/main/java/com/networknt/schema/MinItemsValidator.java
@@ -23,6 +23,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for minItems.
+ */
public class MinItemsValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MinItemsValidator.class);
@@ -40,13 +43,16 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (node.isArray()) {
if (node.size() < min) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, node.size()).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min, node.size())
+ .build());
}
} else if (this.validationContext.getConfig().isTypeLoose()) {
if (1 < min) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, 1).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min, 1).build());
}
}
diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java
index 7038a2bd1..c5dc0435a 100644
--- a/src/main/java/com/networknt/schema/MinLengthValidator.java
+++ b/src/main/java/com/networknt/schema/MinLengthValidator.java
@@ -23,6 +23,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for minLength.
+ */
public class MinLengthValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MinLengthValidator.class);
@@ -46,8 +49,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(minLength).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(minLength).build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java
index 419ca6f1c..46db1447e 100644
--- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java
+++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java
@@ -8,7 +8,7 @@
import java.util.stream.Collectors;
/**
- * Tests the validity of {@literal maxContains} and {@literal minContains} in a schema.
+ * {@link JsonValidator} for {@literal maxContains} and {@literal minContains} in a schema.
*
* This validator only checks that the schema is valid. The functionality for
* testing whether an instance array conforms to the {@literal maxContains}
@@ -62,8 +62,10 @@ public MinMaxContainsValidator(SchemaLocation schemaLocation, JsonNodePath evalu
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
JsonNodePath instanceLocation) {
return this.analysis != null ? this.analysis.stream()
- .map(analysis -> message().instanceLocation(analysis.getSchemaLocation().getFragment())
+ .map(analysis -> message().instanceNode(node)
+ .instanceLocation(analysis.getSchemaLocation().getFragment())
.messageKey(analysis.getMessageKey()).locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
.arguments(parentSchema.getSchemaNode().toString()).build())
.collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java
index 80e41f798..17b4fbe3c 100644
--- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java
@@ -23,6 +23,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for minProperties.
+ */
public class MinPropertiesValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MinPropertiesValidator.class);
@@ -41,8 +44,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (node.isObject()) {
if (node.size() < min) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(min).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min).build());
}
}
diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java
index 27ff40253..3e43624d5 100644
--- a/src/main/java/com/networknt/schema/MinimumValidator.java
+++ b/src/main/java/com/networknt/schema/MinimumValidator.java
@@ -27,6 +27,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for minimum.
+ */
public class MinimumValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MinimumValidator.class);
private static final String PROPERTY_EXCLUSIVE_MINIMUM = "exclusiveMinimum";
@@ -120,9 +123,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
if (typedMinimum.crossesThreshold(node)) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue())
- .build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
+ .arguments(typedMinimum.thresholdValue()).build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java
index 7d1d73f45..e91f2d4e4 100644
--- a/src/main/java/com/networknt/schema/MultipleOfValidator.java
+++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java
@@ -24,6 +24,9 @@
import java.util.Collections;
import java.util.Set;
+/**
+ * {@link JsonValidator} for multipleOf.
+ */
public class MultipleOfValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MultipleOfValidator.class);
@@ -46,8 +49,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue));
BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor));
if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(divisor).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(divisor).build());
}
}
}
diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java
index ef1b8d9f5..c5af300b1 100644
--- a/src/main/java/com/networknt/schema/NonValidationKeyword.java
+++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java
@@ -27,14 +27,19 @@
* Used for Keywords that have no validation aspect, but are part of the metaschema.
*/
public class NonValidationKeyword extends AbstractKeyword {
+ private final boolean collectAnnotations;
private static final class Validator extends AbstractJsonValidator {
+ private final boolean collectAnnotations;
+
public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
- JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) {
- super(schemaLocation, evaluationPath, keyword);
+ JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword, boolean collectAnnotations) {
+ super(schemaLocation, evaluationPath, keyword, schemaNode);
+ this.collectAnnotations = collectAnnotations;
String id = validationContext.resolveSchemaId(schemaNode);
String anchor = validationContext.getMetaSchema().readAnchor(schemaNode);
- if (id != null || anchor != null) {
+ String dynamicAnchor = validationContext.getMetaSchema().readDynamicAnchor(schemaNode);
+ if (id != null || anchor != null || dynamicAnchor != null) {
// Used to register schema resources with $id
validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema);
}
@@ -49,17 +54,40 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
+ if (collectAnnotations && collectAnnotations(executionContext)) {
+ Object value = getAnnotationValue(getSchemaNode());
+ if (value != null) {
+ putAnnotation(executionContext,
+ annotation -> annotation.instanceLocation(instanceLocation).value(value));
+ }
+ }
return Collections.emptySet();
}
+
+ protected Object getAnnotationValue(JsonNode schemaNode) {
+ if (schemaNode.isTextual()) {
+ return schemaNode.textValue();
+ } else if (schemaNode.isNumber()) {
+ return schemaNode.numberValue();
+ } else if (schemaNode.isObject()) {
+ return schemaNode;
+ }
+ return null;
+ }
}
public NonValidationKeyword(String keyword) {
+ this(keyword, true);
+ }
+
+ public NonValidationKeyword(String keyword, boolean collectAnnotations) {
super(keyword);
+ this.collectAnnotations = collectAnnotations;
}
@Override
public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception {
- return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this);
+ return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this, collectAnnotations);
}
}
diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java
index c6c4e80fe..baf8dd762 100644
--- a/src/main/java/com/networknt/schema/NotAllowedValidator.java
+++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java
@@ -22,6 +22,9 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for notAllowed.
+ */
public class NotAllowedValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(NotAllowedValidator.class);
@@ -49,8 +52,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (errors == null) {
errors = new LinkedHashSet<>();
}
- errors.add(message().property(fieldName).instanceLocation(instanceLocation.append(fieldName))
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build());
+ errors.add(message().property(fieldName).instanceNode(node)
+ .instanceLocation(instanceLocation.append(fieldName))
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(fieldName).build());
}
}
diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java
index 6e5164301..9a2c2e36e 100644
--- a/src/main/java/com/networknt/schema/NotValidator.java
+++ b/src/main/java/com/networknt/schema/NotValidator.java
@@ -17,13 +17,15 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} for not.
+ */
public class NotValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(NotValidator.class);
@@ -36,25 +38,25 @@ public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
- CollectorContext collectorContext = executionContext.getCollectorContext();
- Set errors = new HashSet<>();
+ Set errors = null;
+ debug(logger, node, rootNode, instanceLocation);
- Scope parentScope = collectorContext.enterDynamicScope();
+ // Save flag as nested schema evaluation shouldn't trigger fail fast
+ boolean failFast = executionContext.getExecutionConfig().isFailFast();
try {
- debug(logger, node, rootNode, instanceLocation);
+ executionContext.getExecutionConfig().setFailFast(false);
errors = this.schema.validate(executionContext, node, rootNode, instanceLocation);
- if (errors.isEmpty()) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString())
- .build());
- }
- return Collections.emptySet();
} finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
+ // Restore flag
+ executionContext.getExecutionConfig().setFailFast(failFast);
}
+ if (errors.isEmpty()) {
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString())
+ .build());
+ }
+ return Collections.emptySet();
}
@Override
@@ -65,8 +67,9 @@ public Set walk(ExecutionContext executionContext, JsonNode n
Set errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
if (errors.isEmpty()) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString())
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString())
.build());
}
return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index e7e8eb7b4..1fe74a56e 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -17,18 +17,22 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} for oneOf.
+ */
public class OneOfValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(OneOfValidator.class);
private final List schemas = new ArrayList<>();
+ private Boolean canShortCircuit = null;
+
public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext);
int size = schemaNode.size();
@@ -41,85 +45,90 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
Set errors = new LinkedHashSet<>();
- CollectorContext collectorContext = executionContext.getCollectorContext();
- Scope grandParentScope = collectorContext.enterDynamicScope();
- try {
- debug(logger, node, rootNode, instanceLocation);
+ debug(logger, node, rootNode, instanceLocation);
- ValidatorState state = executionContext.getValidatorState();
+ ValidatorState state = executionContext.getValidatorState();
- // this is a complex validator, we set the flag to true
- state.setComplexValidator(true);
+ // this is a complex validator, we set the flag to true
+ state.setComplexValidator(true);
- int numberOfValidSchema = 0;
- Set childErrors = new LinkedHashSet<>();
+ int numberOfValidSchema = 0;
+ Set childErrors = new LinkedHashSet<>();
+ // Save flag as nested schema evaluation shouldn't trigger fail fast
+ boolean failFast = executionContext.getExecutionConfig().isFailFast();
+ try {
+ executionContext.getExecutionConfig().setFailFast(false);
for (JsonSchema schema : this.schemas) {
Set schemaErrors = Collections.emptySet();
- Scope parentScope = collectorContext.enterDynamicScope();
- try {
- // Reset state in case the previous validator did not match
- state.setMatchedNode(true);
-
- if (!state.isWalkEnabled()) {
- schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation);
- } else {
- schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation, state.isValidationEnabled());
- }
-
- // check if any validation errors have occurred
- if (schemaErrors.isEmpty()) {
- // check whether there are no errors HOWEVER we have validated the exact validator
- if (!state.hasMatchedNode())
- continue;
-
- numberOfValidSchema++;
- }
+ // Reset state in case the previous validator did not match
+ state.setMatchedNode(true);
- if (numberOfValidSchema > 1) {
- // short-circuit
- break;
- }
+ if (!state.isWalkEnabled()) {
+ schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation);
+ } else {
+ schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation,
+ state.isValidationEnabled());
+ }
- childErrors.addAll(schemaErrors);
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (schemaErrors.isEmpty()) {
- parentScope.mergeWith(scope);
+ // check if any validation errors have occurred
+ if (schemaErrors.isEmpty()) {
+ // check whether there are no errors HOWEVER we have validated the exact
+ // validator
+ if (!state.hasMatchedNode()) {
+ continue;
}
+ numberOfValidSchema++;
}
- }
- // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
- if (numberOfValidSchema != 1) {
- ValidationMessage message = message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale())
- .arguments(Integer.toString(numberOfValidSchema)).build();
- if (this.failFast) {
- throw new JsonSchemaException(message);
+ if (numberOfValidSchema > 1 && canShortCircuit()) {
+ // short-circuit
+ break;
}
- errors.add(message);
- errors.addAll(childErrors);
- collectorContext.getEvaluatedItems().clear();
- collectorContext.getEvaluatedProperties().clear();
+
+ childErrors.addAll(schemaErrors);
}
+ } finally {
+ // Restore flag
+ executionContext.getExecutionConfig().setFailFast(failFast);
+ }
- // Make sure to signal parent handlers we matched
- if (errors.isEmpty())
- state.setMatchedNode(true);
+ // ensure there is always an "OneOf" error reported if number of valid schemas
+ // is not equal to 1.
+ if (numberOfValidSchema != 1) {
+ ValidationMessage message = message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast())
+ .arguments(Integer.toString(numberOfValidSchema)).build();
+ errors.add(message);
+ errors.addAll(childErrors);
+ }
- // reset the ValidatorState object
- resetValidatorState(executionContext);
+ // Make sure to signal parent handlers we matched
+ if (errors.isEmpty()) {
+ state.setMatchedNode(true);
+ }
- return Collections.unmodifiableSet(errors);
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (errors.isEmpty()) {
- grandParentScope.mergeWith(scope);
+ // reset the ValidatorState object
+ resetValidatorState(executionContext);
+
+ return Collections.unmodifiableSet(errors);
+ }
+
+ protected boolean canShortCircuit() {
+ if (this.canShortCircuit == null) {
+ boolean canShortCircuit = true;
+ for (JsonValidator validator : getEvaluationParentSchema().getValidators()) {
+ if ("unevaluatedProperties".equals(validator.getKeyword())
+ || "unevaluatedItems".equals(validator.getKeyword())) {
+ canShortCircuit = false;
+ }
}
+ this.canShortCircuit = canShortCircuit;
}
+ return this.canShortCircuit;
}
private static void resetValidatorState(ExecutionContext executionContext) {
@@ -146,5 +155,6 @@ public void preloadJsonSchema() {
for (JsonSchema schema: this.schemas) {
schema.initializeValidators();
}
+ canShortCircuit(); // cache the flag
}
}
diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java
index 6f8e84d05..b3c56e7ad 100644
--- a/src/main/java/com/networknt/schema/OutputFormat.java
+++ b/src/main/java/com/networknt/schema/OutputFormat.java
@@ -17,6 +17,11 @@
import java.util.Set;
+import com.networknt.schema.output.HierarchicalOutputUnitFormatter;
+import com.networknt.schema.output.ListOutputUnitFormatter;
+import com.networknt.schema.output.OutputFlag;
+import com.networknt.schema.output.OutputUnit;
+
/**
* Formats the validation results.
*
@@ -37,13 +42,15 @@ default void customize(ExecutionContext executionContext, ValidationContext vali
/**
* Formats the validation results.
*
+ * @param jsonSchema the schema
* @param validationMessages the validation messages
- * @param executionContext the execution context
- * @param validationContext the validation context
+ * @param executionContext the execution context
+ * @param validationContext the validation context
+ *
* @return the result
*/
- T format(Set validationMessages, ExecutionContext executionContext,
- ValidationContext validationContext);
+ T format(JsonSchema jsonSchema, Set validationMessages,
+ ExecutionContext executionContext, ValidationContext validationContext);
/**
* The Default output format.
@@ -59,6 +66,17 @@ T format(Set validationMessages, ExecutionContext executionCo
* The Flag output format.
*/
public static final Flag FLAG = new Flag();
+
+ /**
+ * The List output format.
+ */
+ public static final List LIST = new List();
+
+
+ /**
+ * The Hierarchical output format.
+ */
+ public static final Hierarchical HIERARCHICAL = new Hierarchical();
/**
* The Default output format.
@@ -66,13 +84,12 @@ T format(Set validationMessages, ExecutionContext executionCo
public static class Default implements OutputFormat> {
@Override
public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
- executionContext.getExecutionConfig().setAnnotationAllowedPredicate(
- Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema()));
+ executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false);
}
@Override
- public Set format(Set validationMessages,
- ExecutionContext executionContext, ValidationContext validationContext) {
+ public Set format(JsonSchema jsonSchema,
+ Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) {
return validationMessages;
}
}
@@ -80,17 +97,17 @@ public Set format(Set validationMessages,
/**
* The Flag output format.
*/
- public static class Flag implements OutputFormat {
+ public static class Flag implements OutputFormat {
@Override
public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
- executionContext.getExecutionConfig().setAnnotationAllowedPredicate(
- Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema()));
+ executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false);
+ executionContext.getExecutionConfig().setFailFast(true);
}
@Override
- public FlagOutput format(Set validationMessages, ExecutionContext executionContext,
- ValidationContext validationContext) {
- return new FlagOutput(validationMessages.isEmpty());
+ public OutputFlag format(JsonSchema jsonSchema, Set validationMessages,
+ ExecutionContext executionContext, ValidationContext validationContext) {
+ return new OutputFlag(validationMessages.isEmpty());
}
}
@@ -100,29 +117,44 @@ public FlagOutput format(Set validationMessages, ExecutionCon
public static class Boolean implements OutputFormat {
@Override
public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
- executionContext.getExecutionConfig().setAnnotationAllowedPredicate(
- Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema()));
+ executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false);
+ executionContext.getExecutionConfig().setFailFast(true);
}
@Override
- public java.lang.Boolean format(Set validationMessages, ExecutionContext executionContext,
- ValidationContext validationContext) {
+ public java.lang.Boolean format(JsonSchema jsonSchema, Set validationMessages,
+ ExecutionContext executionContext, ValidationContext validationContext) {
return validationMessages.isEmpty();
}
}
-
+
/**
- * The Flag output results.
+ * The List output format.
*/
- public static class FlagOutput {
- private final boolean valid;
+ public static class List implements OutputFormat {
+ @Override
+ public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
+ }
- public FlagOutput(boolean valid) {
- this.valid = valid;
+ @Override
+ public OutputUnit format(JsonSchema jsonSchema, Set validationMessages,
+ ExecutionContext executionContext, ValidationContext validationContext) {
+ return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext);
}
+ }
- public boolean isValid() {
- return this.valid;
+ /**
+ * The Hierarchical output format.
+ */
+ public static class Hierarchical implements OutputFormat {
+ @Override
+ public void customize(ExecutionContext executionContext, ValidationContext validationContext) {
+ }
+
+ @Override
+ public OutputUnit format(JsonSchema jsonSchema, Set validationMessages,
+ ExecutionContext executionContext, ValidationContext validationContext) {
+ return HierarchicalOutputUnitFormatter.format(jsonSchema, validationMessages, executionContext, validationContext);
}
}
}
diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
index ceeb6c2b8..1e5bc18c9 100644
--- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
@@ -17,17 +17,23 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
+import com.networknt.schema.annotation.JsonNodeAnnotation;
import com.networknt.schema.regex.RegularExpression;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} for patternProperties.
+ */
public class PatternPropertiesValidator extends BaseJsonValidator {
public static final String PROPERTY = "patternProperties";
private static final Logger logger = LoggerFactory.getLogger(PatternPropertiesValidator.class);
private final Map schemas = new IdentityHashMap<>();
+ private Boolean hasUnevaluatedPropertiesValidator = null;
+
public PatternPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema,
ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES, validationContext);
@@ -50,7 +56,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
return Collections.emptySet();
}
Set errors = null;
+ Set matchedInstancePropertyNames = null;
Iterator names = node.fieldNames();
+ boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext);
while (names.hasNext()) {
String name = names.next();
JsonNode n = node.get(name);
@@ -59,8 +67,11 @@ public Set validate(ExecutionContext executionContext, JsonNo
JsonNodePath path = instanceLocation.append(name);
Set results = entry.getValue().validate(executionContext, n, rootNode, path);
if (results.isEmpty()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- executionContext.getCollectorContext().getEvaluatedProperties().add(path);
+ if (collectAnnotations) {
+ if (matchedInstancePropertyNames == null) {
+ matchedInstancePropertyNames = new LinkedHashSet<>();
+ }
+ matchedInstancePropertyNames.add(name);
}
} else {
if (errors == null) {
@@ -71,11 +82,29 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
}
}
+ if (collectAnnotations) {
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(matchedInstancePropertyNames).build());
+ }
return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors);
}
+
+ private boolean collectAnnotations() {
+ return hasUnevaluatedPropertiesValidator();
+ }
+
+ private boolean hasUnevaluatedPropertiesValidator() {
+ if (this.hasUnevaluatedPropertiesValidator == null) {
+ this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties");
+ }
+ return hasUnevaluatedPropertiesValidator;
+ }
@Override
public void preloadJsonSchema() {
preloadJsonSchemas(schemas.values());
+ collectAnnotations(); // cache the flag
}
}
diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java
index 71247cf95..9b7f087a8 100644
--- a/src/main/java/com/networknt/schema/PatternValidator.java
+++ b/src/main/java/com/networknt/schema/PatternValidator.java
@@ -58,8 +58,9 @@ public Set validate(ExecutionContext executionContext, JsonNo
try {
if (!matches(node.asText())) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.pattern).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.pattern).build());
}
} catch (JsonSchemaException e) {
throw e;
diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
index dc2b00a55..230f67798 100644
--- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java
+++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java
@@ -18,23 +18,28 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.networknt.schema.annotation.JsonNodeAnnotation;
import com.networknt.schema.walk.DefaultItemWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
+/**
+ * {@link JsonValidator} for prefixItems.
+ */
public class PrefixItemsValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(PrefixItemsValidator.class);
private final List tupleSchema;
private WalkListenerRunner arrayItemWalkListenerRunner;
+
+ private Boolean hasUnevaluatedItemsValidator = null;
public PrefixItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PREFIX_ITEMS, validationContext);
@@ -58,19 +63,34 @@ public Set validate(ExecutionContext executionContext, JsonNo
// ignores non-arrays
if (node.isArray()) {
Set errors = new LinkedHashSet<>();
- Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems();
int count = Math.min(node.size(), this.tupleSchema.size());
for (int i = 0; i < count; ++i) {
JsonNodePath path = instanceLocation.append(i);
Set results = this.tupleSchema.get(i).validate(executionContext, node.get(i), rootNode, path);
- if (results.isEmpty()) {
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- evaluatedItems.add(path);
- }
- } else {
+ if (!results.isEmpty()) {
errors.addAll(results);
}
}
+
+ // Add annotation
+ if (collectAnnotations() || collectAnnotations(executionContext)) {
+ // Tuples
+ int items = node.isArray() ? node.size() : 1;
+ int schemas = this.tupleSchema.size();
+ if (items > schemas) {
+ // More items than schemas so the keyword only applied to the number of schemas
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(schemas).build());
+ } else {
+ // Applies to all
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(true).build());
+ }
+ }
return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors);
} else {
return Collections.emptySet();
@@ -140,9 +160,21 @@ public List getTupleSchema() {
return this.tupleSchema;
}
+ private boolean collectAnnotations() {
+ return hasUnevaluatedItemsValidator();
+ }
+
+ private boolean hasUnevaluatedItemsValidator() {
+ if (this.hasUnevaluatedItemsValidator == null) {
+ this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems");
+ }
+ return hasUnevaluatedItemsValidator;
+ }
+
@Override
public void preloadJsonSchema() {
preloadJsonSchemas(this.tupleSchema);
+ collectAnnotations(); // cache the flag
}
}
diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java
index cd6534f74..f3ce77719 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -19,6 +19,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.annotation.JsonNodeAnnotation;
import com.networknt.schema.walk.DefaultPropertyWalkListenerRunner;
import com.networknt.schema.walk.WalkListenerRunner;
import org.slf4j.Logger;
@@ -26,10 +27,15 @@
import java.util.*;
+/**
+ * {@link JsonValidator} for properties.
+ */
public class PropertiesValidator extends BaseJsonValidator {
public static final String PROPERTY = "properties";
private static final Logger logger = LoggerFactory.getLogger(PropertiesValidator.class);
private final Map schemas = new LinkedHashMap<>();
+
+ private Boolean hasUnevaluatedPropertiesValidator;
public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext);
@@ -43,7 +49,6 @@ public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluatio
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
debug(logger, node, rootNode, instanceLocation);
- CollectorContext collectorContext = executionContext.getCollectorContext();
WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(this.validationContext.getConfig().getPropertyWalkListeners());
@@ -52,15 +57,19 @@ public Set validate(ExecutionContext executionContext, JsonNo
// get the Validator state object storing validation data
ValidatorState state = executionContext.getValidatorState();
- Set requiredErrors = null;
-
+ Set requiredErrors = null;
+ Set matchedInstancePropertyNames = null;
+ boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext);
for (Map.Entry entry : this.schemas.entrySet()) {
JsonSchema propertySchema = entry.getValue();
JsonNode propertyNode = node.get(entry.getKey());
if (propertyNode != null) {
JsonNodePath path = instanceLocation.append(entry.getKey());
- if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) {
- collectorContext.getEvaluatedProperties().add(path); // TODO: This should happen after validation
+ if (collectAnnotations) {
+ if (matchedInstancePropertyNames == null) {
+ matchedInstancePropertyNames = new LinkedHashSet<>();
+ }
+ matchedInstancePropertyNames.add(entry.getKey());
}
// check whether this is a complex validator. save the state
boolean isComplex = state.isComplexValidator();
@@ -70,7 +79,7 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
// reset the complex validator for child element validation, and reset it after the return from the recursive call
state.setComplexValidator(false);
-
+
if (!state.isWalkEnabled()) {
//validate the child element(s)
Set result = propertySchema.validate(executionContext, propertyNode, rootNode, path);
@@ -119,6 +128,15 @@ public Set validate(ExecutionContext executionContext, JsonNo
}
}
}
+ if (collectAnnotations) {
+ executionContext.getAnnotations()
+ .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
+ .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
+ .keyword(getKeyword()).value(matchedInstancePropertyNames == null ? Collections.emptySet()
+ : matchedInstancePropertyNames)
+ .build());
+ }
+
return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors);
}
@@ -139,6 +157,17 @@ public Set walk(ExecutionContext executionContext, JsonNode n
return validationMessages;
}
+ private boolean collectAnnotations() {
+ return hasUnevaluatedPropertiesValidator();
+ }
+
+ private boolean hasUnevaluatedPropertiesValidator() {
+ if (this.hasUnevaluatedPropertiesValidator == null) {
+ this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties");
+ }
+ return hasUnevaluatedPropertiesValidator;
+ }
+
private void applyPropertyDefaults(ObjectNode node) {
for (Map.Entry entry : this.schemas.entrySet()) {
JsonNode propertyNode = node.get(entry.getKey());
@@ -188,5 +217,6 @@ public Map getSchemas() {
@Override
public void preloadJsonSchema() {
preloadJsonSchemas(this.schemas.values());
+ collectAnnotations(); // cache the flag
}
}
diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
index 96d9ed3ea..12465d34b 100644
--- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
@@ -48,8 +48,10 @@ public Set validate(ExecutionContext executionContext, JsonNo
if (msg.startsWith(path))
msg = msg.substring(path.length()).replaceFirst("^:\\s*", "");
- errors.add(message().property(pname).instanceLocation(schemaError.getInstanceLocation())
- .locale(executionContext.getExecutionConfig().getLocale()).arguments(msg).build());
+ errors.add(
+ message().property(pname).instanceNode(node).instanceLocation(schemaError.getInstanceLocation())
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(msg).build());
}
}
return Collections.unmodifiableSet(errors);
diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java
index a3d1e4f22..bf2759bfe 100644
--- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java
+++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java
@@ -24,6 +24,9 @@
import com.fasterxml.jackson.databind.JsonNode;
+/**
+ * {@link JsonValidator} for readOnly.
+ */
public class ReadOnlyValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(ReadOnlyValidator.class);
@@ -40,8 +43,9 @@ public ReadOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
debug(logger, node, rootNode, instanceLocation);
if (this.readOnly) {
- return Collections.singleton(message().instanceLocation(instanceLocation)
- .locale(executionContext.getExecutionConfig().getLocale()).build());
+ return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
+ .locale(executionContext.getExecutionConfig().getLocale())
+ .failFast(executionContext.getExecutionConfig().isFailFast()).build());
}
return Collections.emptySet();
}
diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java
index 16714be22..0ca0f26fb 100644
--- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java
+++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java
@@ -17,84 +17,132 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} that resolves $recursiveRef.
+ */
public class RecursiveRefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class);
- private Map schemas = new HashMap<>();
+ protected JsonSchemaRef schema;
public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext);
String refValue = schemaNode.asText();
if (!"#".equals(refValue)) {
- ValidationMessage validationMessage = ValidationMessage.builder()
+ ValidationMessage validationMessage = message()
.type(ValidatorTypeCode.RECURSIVE_REF.getValue()).code("internal.invalidRecursiveRef")
.message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").instanceLocation(schemaLocation.getFragment())
- .evaluationPath(schemaLocation.getFragment()).arguments(refValue).build();
+ .instanceNode(this.schemaNode)
+ .evaluationPath(evaluationPath).arguments(refValue).build();
throw new JsonSchemaException(validationMessage);
}
+ this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath);
}
+ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue,
+ JsonNodePath evaluationPath) {
+ return new JsonSchemaRef(new CachedSupplier<>(() -> {
+ return getSchema(parentSchema, validationContext, refValue, evaluationPath);
+ }));
+ }
+
+ static JsonSchema getSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue,
+ JsonNodePath evaluationPath) {
+ JsonSchema refSchema = parentSchema.findSchemaResourceRoot(); // Get the document
+ JsonSchema current = refSchema;
+ JsonSchema check = null;
+ String base = null;
+ String baseCheck = null;
+ if (refSchema != null)
+ base = current.getSchemaLocation().getAbsoluteIri() != null ? current.getSchemaLocation().getAbsoluteIri().toString() : "";
+ if (current.isRecursiveAnchor()) {
+ // Check dynamic scope
+ while (current.getEvaluationParentSchema() != null) {
+ current = current.getEvaluationParentSchema();
+ baseCheck = current.getSchemaLocation().getAbsoluteIri() != null ? current.getSchemaLocation().getAbsoluteIri().toString() : "";
+ if (!base.equals(baseCheck)) {
+ base = baseCheck;
+ // Check if it has a dynamic anchor
+ check = current.findSchemaResourceRoot();
+ if (check.isRecursiveAnchor()) {
+ refSchema = check;
+ }
+ }
+ }
+ }
+ if (refSchema != null) {
+ refSchema = refSchema.fromRef(parentSchema, evaluationPath);
+ }
+ return refSchema;
+ }
+
@Override
public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
- CollectorContext collectorContext = executionContext.getCollectorContext();
-
- Set errors = new HashSet<>();
-
- Scope parentScope = collectorContext.enterDynamicScope();
- try {
- debug(logger, node, rootNode, instanceLocation);
-
- JsonSchema schema = collectorContext.getOutermostSchema();
- if (null != schema) {
- JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> {
- return schema.fromRef(getParentSchema(), getEvaluationPath());
- });
- errors = refSchema.validate(executionContext, node, rootNode, instanceLocation);
- }
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
+ debug(logger, node, rootNode, instanceLocation);
+ JsonSchema refSchema = this.schema.getSchema();
+ if (refSchema == null) {
+ ValidationMessage validationMessage = message().type(ValidatorTypeCode.RECURSIVE_REF.getValue())
+ .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
+ .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
+ .arguments(schemaNode.asText()).build();
+ throw new InvalidSchemaRefException(validationMessage);
}
-
- return errors;
+ return refSchema.validate(executionContext, node, rootNode, instanceLocation);
}
@Override
public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
- CollectorContext collectorContext = executionContext.getCollectorContext();
+ debug(logger, node, rootNode, instanceLocation);
+ // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances,
+ // these schemas will be cached along with config. We have to replace the config for cached $ref references
+ // with the latest config. Reset the config.
+ JsonSchema refSchema = this.schema.getSchema();
+ if (refSchema == null) {
+ ValidationMessage validationMessage = message().type(ValidatorTypeCode.RECURSIVE_REF.getValue())
+ .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved")
+ .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath())
+ .arguments(schemaNode.asText()).build();
+ throw new InvalidSchemaRefException(validationMessage);
+ }
+ return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
+ }
- Set errors = new HashSet<>();
+ public JsonSchemaRef getSchemaRef() {
+ return this.schema;
+ }
- Scope parentScope = collectorContext.enterDynamicScope();
+ @Override
+ public void preloadJsonSchema() {
+ JsonSchema jsonSchema = null;
try {
- debug(logger, node, rootNode, instanceLocation);
-
- JsonSchema schema = collectorContext.getOutermostSchema();
- if (null != schema) {
- JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> {
- return schema.fromRef(getParentSchema(), getEvaluationPath());
- });
- errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
- }
- } finally {
- Scope scope = collectorContext.exitDynamicScope();
- if (shouldValidateSchema) {
- if (errors.isEmpty()) {
- parentScope.mergeWith(scope);
- }
+ jsonSchema = this.schema.getSchema();
+ } catch (JsonSchemaException e) {
+ throw e;
+ } catch (RuntimeException e) {
+ throw new JsonSchemaException(e);
+ }
+ // Check for circular dependency
+ // Only one cycle is pre-loaded
+ // The rest of the cycles will load at execution time depending on the input
+ // data
+ SchemaLocation schemaLocation = jsonSchema.getSchemaLocation();
+ JsonSchema check = jsonSchema;
+ boolean circularDependency = false;
+ while (check.getEvaluationParentSchema() != null) {
+ check = check.getEvaluationParentSchema();
+ if (check.getSchemaLocation().equals(schemaLocation)) {
+ circularDependency = true;
+ break;
}
}
-
- return errors;
+ if (!circularDependency) {
+ jsonSchema.initializeValidators();
+ }
}
-
}
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index e8c5016e1..1959231e3 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -17,12 +17,14 @@
package com.networknt.schema;
import com.fasterxml.jackson.databind.JsonNode;
-import com.networknt.schema.CollectorContext.Scope;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+/**
+ * {@link JsonValidator} that resolves $ref.
+ */
public class RefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(RefValidator.class);
@@ -77,7 +79,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val
findSchemaResource = validationContext.getDynamicAnchors().get(find);
}
if (findSchemaResource != null) {
- schemaResource = findSchemaResource;
+ schemaResource = findSchemaResource;
} else {
schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal,
evaluationPath);
@@ -147,75 +149,59 @@ private static JsonSchema getJsonSchema(JsonSchema parent,
// This should be processing json pointer fragments only
JsonNodePath fragment = SchemaLocation.Fragment.of(refValue);
String schemaReference = resolve(parent, refValueOriginal);
- return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> {
- return parent.getSubSchema(fragment);
- });
+ // ConcurrentHashMap computeIfAbsent does not allow calls that result in a
+ // recursive update to the map.
+ // The getSubSchema potentially recurses to call back to getJsonSchema again
+ JsonSchema result = validationContext.getSchemaReferences().get(schemaReference);
+ if (result == null) {
+ synchronized (validationContext.getJsonSchemaFactory()) { // acquire lock on shared factory object to prevent deadlock
+ result = validationContext.getSchemaReferences().get(schemaReference);
+ if (result == null) {
+ result = parent.getSubSchema(fragment);
+ if (result != null) {
+ validationContext.getSchemaReferences().put(schemaReference, result);
+ }
+ }
+ }
+ }
+ return result;
}
@Override
public Set