From d3d2ba416b2fe1889d7a52f451ea4295206a825d Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 19 Dec 2023 17:14:50 +0800 Subject: [PATCH 01/53] Support annotations --- .../schema/AdditionalPropertiesValidator.java | 19 +- .../networknt/schema/CollectorContext.java | 86 ++----- .../networknt/schema/ContainsValidator.java | 77 ++++-- .../networknt/schema/ExecutionContext.java | 12 + .../com/networknt/schema/ItemsValidator.java | 70 ++++-- .../schema/ItemsValidator202012.java | 16 +- .../java/com/networknt/schema/JsonSchema.java | 33 +-- .../com/networknt/schema/OneOfValidator.java | 4 +- .../schema/PatternPropertiesValidator.java | 11 +- .../schema/PrefixItemsValidator.java | 25 +- .../networknt/schema/PropertiesValidator.java | 18 +- .../com/networknt/schema/TypeValidator.java | 14 +- .../schema/UnevaluatedItemsValidator.java | 233 ++++++++++++++++-- .../UnevaluatedPropertiesValidator.java | 145 +++++++++-- .../schema/annotation/JsonNodeAnnotation.java | 162 ++++++++++++ .../JsonNodeAnnotationPredicate.java | 156 ++++++++++++ .../annotation/JsonNodeAnnotations.java | 197 +++++++++++++++ .../schema/assertion/JsonNodeAssertions.java | 108 ++++++++ src/main/resources/jsv-messages.properties | 2 +- .../schema/JsonNodeAnnotationsTest.java | 68 +++++ .../unevaluatedTests/unevaluated-tests.json | 4 +- 21 files changed, 1260 insertions(+), 200 deletions(-) create mode 100644 src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java create mode 100644 src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java create mode 100644 src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java create mode 100644 src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java create mode 100644 src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 42994d3b1..e64b31632 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -17,6 +17,7 @@ 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; @@ -71,13 +72,15 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } + Set matchedInstancePropertyNames = new LinkedHashSet<>(); + CollectorContext collectorContext = executionContext.getCollectorContext(); - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - // if allowAdditionalProperties is true, add all the properties as evaluated. - if (allowAdditionalProperties) { - for (Iterator it = node.fieldNames(); it.hasNext(); ) { - collectorContext.getEvaluatedProperties().add(instanceLocation.append(it.next())); - } + // if allowAdditionalProperties is true, add all the properties as evaluated. + if (allowAdditionalProperties) { + for (Iterator it = node.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + matchedInstancePropertyNames.add(fieldName); +// collectorContext.getEvaluatedProperties().add(instanceLocation.resolve(fieldName)); } } @@ -128,6 +131,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + 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); } diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 07b0c6800..59feef82b 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -43,16 +43,12 @@ public class CollectorContext { private Map collectorLoadMap = new HashMap<>(); private final Deque dynamicScopes = new LinkedList<>(); - private final boolean disableUnevaluatedItems; - private final boolean disableUnevaluatedProperties; public CollectorContext() { this(false, false); } public CollectorContext(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties) { - this.disableUnevaluatedItems = disableUnevaluatedItems; - this.disableUnevaluatedProperties = disableUnevaluatedProperties; this.dynamicScopes.push(newTopScope()); } @@ -114,24 +110,6 @@ public JsonSchema getOutermostSchema() { return context.findLexicalRoot(); } - /** - * Identifies which array items have been evaluated. - * - * @return the set of evaluated items (never null) - */ - public Collection getEvaluatedItems() { - return getDynamicScope().getEvaluatedItems(); - } - - /** - * Identifies which properties have been evaluated. - * - * @return the set of evaluated properties (never null) - */ - public Collection getEvaluatedProperties() { - return getDynamicScope().getEvaluatedProperties(); - } - /** * Adds a collector with give name. Preserving this method for backward * compatibility. @@ -226,38 +204,26 @@ void loadCollectors() { } private Scope newScope(JsonSchema containingSchema) { - return new Scope(this.disableUnevaluatedItems, this.disableUnevaluatedProperties, containingSchema); + return new Scope(containingSchema); } private Scope newTopScope() { - return new Scope(true, this.disableUnevaluatedItems, this.disableUnevaluatedProperties, null); + return new Scope(true, null); } public static class Scope { private final JsonSchema containingSchema; - /** - * Used to track which array items have been evaluated. - */ - private final Collection evaluatedItems; - - /** - * Used to track which properties have been evaluated. - */ - private final Collection evaluatedProperties; - private final boolean top; - Scope(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties, JsonSchema containingSchema) { - this(false, disableUnevaluatedItems, disableUnevaluatedProperties, containingSchema); + Scope(JsonSchema containingSchema) { + this(false, containingSchema); } - Scope(boolean top, boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties, JsonSchema containingSchema) { + Scope(boolean top, JsonSchema containingSchema) { this.top = top; this.containingSchema = containingSchema; - this.evaluatedItems = newCollection(disableUnevaluatedItems); - this.evaluatedProperties = newCollection(disableUnevaluatedProperties); } private static Collection newCollection(boolean disabled) { @@ -294,47 +260,25 @@ public JsonSchema getContainingSchema() { return this.containingSchema; } - /** - * Identifies which array items have been evaluated. - * - * @return the set of evaluated items (never null) - */ - public Collection getEvaluatedItems() { - return this.evaluatedItems; - } - - /** - * Identifies which properties have been evaluated. - * - * @return the set of evaluated properties (never null) - */ - public Collection getEvaluatedProperties() { - return this.evaluatedProperties; - } - /** * Merges the provided scope into this scope. * @param scope the scope to merge * @return this scope */ public Scope mergeWith(Scope scope) { - if (!scope.getEvaluatedItems().isEmpty()) { - getEvaluatedItems().addAll(scope.getEvaluatedItems()); - } - if (!scope.getEvaluatedProperties().isEmpty()) { - getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); - } +// getEvaluatedItems().addAll(scope.getEvaluatedItems()); +// getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); return this; } - @Override - public String toString() { - return new StringBuilder("{ ") - .append("\"evaluatedItems\": ").append(this.evaluatedItems) - .append(", ") - .append("\"evaluatedProperties\": ").append(this.evaluatedProperties) - .append(" }").toString(); - } +// @Override +// public String toString() { +// return new StringBuilder("{ ") +// .append("\"evaluatedItems\": ").append(this.evaluatedItems) +// .append(", ") +// .append("\"evaluatedProperties\": ").append(this.evaluatedProperties) +// .append(" }").toString(); +// } } } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index fb0268187..dd4a193df 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -18,11 +18,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -38,8 +42,8 @@ public class ContainsValidator extends BaseJsonValidator { private final JsonSchema schema; private final boolean isMinV201909; - private int min = 1; - private int max = Integer.MAX_VALUE; + private Integer min = null; + private Integer max = null; public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext); @@ -69,40 +73,83 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); // ignores non-arrays + Set results = null; + int actual = 0, i = 0; + List indexes = new ArrayList<>(); // for the annotation if (null != this.schema && node.isArray()) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); +// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); - int actual = 0, i = 0; for (JsonNode n : node) { JsonNodePath path = instanceLocation.append(i); - if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { ++actual; - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } +// evaluatedItems.add(path); + indexes.add(i); } ++i; } - - if (actual < this.min) { + int m = 1; // default to 1 if "min" not specified + if (this.min != null) { + m = this.min; + } + if (actual < m) { if(isMinV201909) { updateValidatorType(ValidatorTypeCode.MIN_CONTAINS); } - return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), - executionContext.getExecutionConfig().getLocale(), instanceLocation, this.min); + results = boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), + executionContext.getExecutionConfig().getLocale(), instanceLocation, m); } - if (actual > this.max) { + if (this.max != null && actual > this.max) { if(isMinV201909) { updateValidatorType(ValidatorTypeCode.MAX_CONTAINS); } - return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), + results = boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), executionContext.getExecutionConfig().getLocale(), instanceLocation, this.max); } } - return Collections.emptySet(); + if (this.schema != null) { + // This keyword produces an annotation value which is an array of the indexes to + // which this keyword validates successfully when applying its subschema, in + // ascending order. The value MAY be a boolean "true" if the subschema validates + // successfully when applied to every index of the instance. The annotation MUST + // be present if the instance array to which this keyword's schema applies is + // empty. + if (actual == i) { + // evaluated all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } else { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(indexes).build()); + } + // Add minContains and maxContains annotations + if (this.min != null) { + // Omitted keywords MUST NOT produce annotation results. However, as described + // in the section for contains, the absence of this keyword's annotation causes + // contains to assume a minimum value of 1. + String minContainsKeyword = "minContains"; + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(minContainsKeyword)) + .schemaLocation(this.schemaLocation.append(minContainsKeyword)) + .keyword(minContainsKeyword).value(this.min).build()); + } + if (this.max != null) { + String maxContainsKeyword = "maxContains"; + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(maxContainsKeyword)) + .schemaLocation(this.schemaLocation.append(maxContainsKeyword)) + .keyword(maxContainsKeyword).value(this.max).build()); + } + } + return results == null ? Collections.emptySet() : results; } @Override diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index c3129eb03..662e9ae17 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -16,6 +16,8 @@ package com.networknt.schema; +import com.networknt.schema.annotation.JsonNodeAnnotations; +import com.networknt.schema.assertion.JsonNodeAssertions; import java.util.Stack; /** @@ -26,6 +28,8 @@ public class ExecutionContext { private CollectorContext collectorContext; private ValidatorState validatorState = null; private Stack discriminatorContexts = new Stack<>(); + private JsonNodeAnnotations annotations = new JsonNodeAnnotations(); + private JsonNodeAssertions assertions = new JsonNodeAssertions(); /** * Creates an execution context. @@ -99,6 +103,14 @@ public void setExecutionConfig(ExecutionConfig executionConfig) { this.executionConfig = executionConfig; } + public JsonNodeAnnotations getAnnotations() { + return annotations; + } + + public JsonNodeAssertions getAssertions() { + return assertions; + } + /** * Gets the validator state. * diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 822406486..53fd0fc5e 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; @@ -78,22 +79,62 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays return Collections.emptySet(); } + + // Add items annotation + 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) { + 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; +// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); JsonNodePath path = instanceLocation.append(i); if (this.schema != null) { @@ -101,9 +142,7 @@ private void doValidate(ExecutionContext executionContext, Set results = this.schema.validate(executionContext, node, rootNode, path); if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } +// evaluatedItems.add(path); } else { errors.addAll(results); } @@ -112,28 +151,26 @@ private void doValidate(ExecutionContext executionContext, Set results = this.tupleSchema.get(i).validate(executionContext, node, rootNode, path); if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } +// evaluatedItems.add(path); } else { 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); - } +// evaluatedItems.add(path); } else { 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) @@ -144,6 +181,7 @@ private void doValidate(ExecutionContext executionContext, 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) { + // 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 { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 435b07027..d39f3de66 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -517,6 +517,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Scope parentScope = collectorContext.enterDynamicScope(this); try { results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); + results.forEach(executionContext.getAssertions()::put); } finally { Scope scope = collectorContext.exitDynamicScope(); if (results == null || results.isEmpty()) { @@ -526,14 +527,14 @@ public Set validate(ExecutionContext executionContext, JsonNo 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()); - } +// 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()); +// } } } @@ -977,14 +978,14 @@ public Set walk(ExecutionContext executionContext, JsonNode n 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()); - } +// 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()); +// } } } } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index e7e8eb7b4..2e1fea253 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -102,8 +102,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } errors.add(message); errors.addAll(childErrors); - collectorContext.getEvaluatedItems().clear(); - collectorContext.getEvaluatedProperties().clear(); +// collectorContext.getEvaluatedItems().clear(); +// collectorContext.getEvaluatedProperties().clear(); } // Make sure to signal parent handlers we matched diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index ceeb6c2b8..eb6af8a34 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -17,6 +17,7 @@ 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; @@ -50,6 +51,7 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } Set errors = null; + Set matchedInstancePropertyNames = new LinkedHashSet<>(); Iterator names = node.fieldNames(); while (names.hasNext()) { String name = names.next(); @@ -59,9 +61,8 @@ 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); - } + matchedInstancePropertyNames.add(name); +// executionContext.getCollectorContext().getEvaluatedProperties().add(path); } else { if (errors == null) { errors = new LinkedHashSet<>(); @@ -71,6 +72,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + 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); } diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index dc2b00a55..31ad2fbde 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.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; import org.slf4j.Logger; @@ -58,19 +59,35 @@ 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(); 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); - } +// evaluatedItems.add(path); } else { errors.addAll(results); } } + + // Add annotation + // 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(); diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index cd6534f74..964f8e861 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; @@ -43,7 +44,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,16 +52,15 @@ 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 = new LinkedHashSet<>(); 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 - } +// collectorContext.getEvaluatedProperties().add(path); // TODO: This should happen after validation + matchedInstancePropertyNames.add(entry.getKey()); // check whether this is a complex validator. save the state boolean isComplex = state.isComplexValidator(); // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated @@ -70,7 +69,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 +118,11 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(matchedInstancePropertyNames) + .build()); + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index d780c80f2..3e4e62f08 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -65,13 +65,13 @@ public Set validate(ExecutionContext executionContext, JsonNo // TODO: Is this really necessary? // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} // Hack to catch patternProperties like "^foo":"value" - if (this.schemaLocation.getFragment().getName(-1).equals("type")) { - if (rootNode.isArray()) { - executionContext.getCollectorContext().getEvaluatedItems().add(instanceLocation); - } else if (rootNode.isObject()) { - executionContext.getCollectorContext().getEvaluatedProperties().add(instanceLocation); - } - } +// if (this.schemaLocation.getName(-1).equals("type")) { +// if (rootNode.isArray()) { +// executionContext.getCollectorContext().getEvaluatedItems().add(instanceLocation); +// } else if (rootNode.isObject()) { +// executionContext.getCollectorContext().getEvaluatedProperties().add(instanceLocation); +// } +// } return Collections.emptySet(); } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 415ff5aa9..5f07dc6f7 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -17,20 +17,32 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; +import static com.networknt.schema.VersionCode.MinV202012; + public class UnevaluatedItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedItemsValidator.class); private final JsonSchema schema; - public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, validationContext); + private final boolean isMinV202012; + private static final VersionFlag DEFAULT_VERSION = VersionFlag.V201909; + public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, + validationContext); + isMinV202012 = MinV202012.getVersions().contains(SpecVersionDetector + .detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION)); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { @@ -47,6 +59,177 @@ public Set validate(ExecutionContext executionContext, JsonNo collectorContext.exitDynamicScope(); try { + String itemsKeyword = "items"; + String additionalItemsKeyword = "additionalItems"; + if (isMinV202012) { + /* + * Keywords renamed in 2020-12 + * + * items -> prefixItems additionalItems -> items + */ + itemsKeyword = "prefixItems"; + additionalItemsKeyword = "items"; + } + + boolean valid = false; + int validCount = 0; + + // This indicates whether the "unevaluatedItems" subschema was used for + // evaluated for setting the annotation + boolean evaluated = false; + + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + for (JsonNodePath e : executionContext.getAssertions().asMap().keySet()) { + if (e.getParent().startsWith(a.getEvaluationPath()) + || a.getEvaluationPath().startsWith(e.getParent())) { + // Invalid + return false; + } + } + return true; + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + Map> instanceLocationAnnotations = executionContext + .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyMap()); + + // If schema is "unevaluatedItems: true" this is valid + if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { + valid = true; + // No need to actually evaluate since the schema is true but if there are any + // items the annotation needs to be set + if (node.size() > 0) { + evaluated = true; + } + } else { + // Get all the "items" for the instanceLocation + List items = instanceLocationAnnotations + .getOrDefault(itemsKeyword, Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + if (items.isEmpty()) { + // The "items" wasn't applied meaning it is unevaluated if there is content + valid = false; + } else { + // Annotation results for "items" keywords from multiple schemas applied to the + // same instance location are combined by setting the combined result to true if + // any of the values are true, and otherwise retaining the largest numerical + // value. + for (JsonNodeAnnotation annotation : items) { + if (annotation.getValue() instanceof Number) { + Number value = annotation.getValue(); + int existing = value.intValue(); + if (existing > validCount) { + validCount = existing; + } + } else if (annotation.getValue() instanceof Boolean) { + // The annotation "items: true" + valid = true; + } + } + } + if (!valid) { + // Check the additionalItems annotation + // If the "additionalItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "additionalItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + List additionalItems = instanceLocationAnnotations + .getOrDefault(additionalItemsKeyword, Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalItems) { + if (annotation.getValue() instanceof Boolean + && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "additionalItems: true" + valid = true; + } + } + } + if (!valid) { + // Unevaluated + // Check if there are any "unevaluatedItems" annotations + List unevaluatedItems = instanceLocationAnnotations + .getOrDefault("unevaluatedItems", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedItems) { + if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "unevaluatedItems: true" + valid = true; + } + } + } + } + Set messages = null; + if (!valid) { + // Get all the "contains" for the instanceLocation + List contains = instanceLocationAnnotations + .getOrDefault("contains", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter) + .collect(Collectors.toList()); + + Set containsEvaluated = new HashSet<>(); + boolean containsEvaluatedAll = false; + for (JsonNodeAnnotation a : contains) { + if (a.getValue() instanceof List) { + List values = a.getValue(); + containsEvaluated.addAll(values); + } else if (a.getValue() instanceof Boolean) { + containsEvaluatedAll = true; + } + } + + messages = new LinkedHashSet<>(); + if (!containsEvaluatedAll) { + // Start evaluating from the valid count + for (int x = validCount; x < node.size(); x++) { + // The schema is either "false" or an object schema + if (!containsEvaluated.contains(x)) { + messages.addAll(this.schema.validate(executionContext, node.get(x), node, + instanceLocation.append(x))); + evaluated = true; + } + } + } + if (messages.isEmpty()) { + valid = true; + } else { + // Report these as unevaluated paths or not matching the unevalutedItems schema + messages = messages.stream() + .map(m -> message().instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + } + // If the "unevaluatedItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "unevaluatedItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + if (evaluated) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("unevaluatedItems").value(true).build()); + } + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; + /* + if (!valid) { + System.out.println(instanceLocation + " " + node); + throw new IllegalArgumentException(instanceLocation + " " + node); + }*/ + +/* Set allPaths = allPaths(node, instanceLocation); // Short-circuit since schema is 'true' @@ -78,32 +261,34 @@ public Set validate(ExecutionContext executionContext, JsonNo } return Collections.emptySet(); + */ } finally { collectorContext.enterDynamicScope(); } } - private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { - Set collector = new LinkedHashSet<>(); - int size = node.size(); - for (int i = 0; i < size; ++i) { - JsonNodePath path = instanceLocation.append(i); - collector.add(path); - } - return collector; - } - - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - return unevaluatedPaths - .stream().map(path -> message().instanceLocation(path) - .locale(executionContext.getExecutionConfig().getLocale()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new HashSet<>(allPaths); - unevaluatedProperties.removeAll(collectorContext.getEvaluatedItems()); - return unevaluatedProperties; - } +// private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { +// Set collector = new LinkedHashSet<>(); +// int size = node.size(); +// for (int i = 0; i < size; ++i) { +// JsonNodePath path = instanceLocation.resolve(i); +// collector.add(path); +// } +// return collector; +// } +// +// private Set reportUnevaluatedPaths(Set unevaluatedPaths, +// ExecutionContext executionContext) { +// return unevaluatedPaths +// .stream().map(path -> message().instanceLocation(path) +// .locale(executionContext.getExecutionConfig().getLocale()).build()) +// .collect(Collectors.toCollection(LinkedHashSet::new)); +// } +// +// private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { +// Set unevaluatedProperties = new HashSet<>(allPaths); +// unevaluatedProperties.removeAll(collectorContext.getEvaluatedItems()); +// return unevaluatedProperties; +// } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index ddd676cd8..4cf0616ba 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -17,10 +17,13 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; public class UnevaluatedPropertiesValidator extends BaseJsonValidator { @@ -40,13 +43,112 @@ public UnevaluatedPropertiesValidator(SchemaLocation schemaLocation, JsonNodePat @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isObject()) return Collections.emptySet(); + if (!node.isObject()) return Collections.emptySet(); debug(logger, node, rootNode, instanceLocation); CollectorContext collectorContext = executionContext.getCollectorContext(); collectorContext.exitDynamicScope(); try { + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + for (JsonNodePath e : executionContext.getAssertions().asMap().keySet()) { + if (e.getParent().startsWith(a.getEvaluationPath()) + || a.getEvaluationPath().startsWith(e.getParent())) { + // Invalid + return false; + } + } + return true; + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + Map> instanceLocationAnnotations = executionContext + .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyMap()); + + Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema + Set existingEvaluatedProperties = new LinkedHashSet<>(); + // Get all the "properties" for the instanceLocation + List properties = instanceLocationAnnotations + .getOrDefault("properties", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : properties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } + + // Get all the "patternProperties" for the instanceLocation + List patternProperties = instanceLocationAnnotations + .getOrDefault("patternProperties", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : patternProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } + + // Get all the "patternProperties" for the instanceLocation + List additionalProperties = instanceLocationAnnotations + .getOrDefault("additionalProperties", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } + + // Get all the "unevaluatedProperties" for the instanceLocation + List unevaluatedProperties = instanceLocationAnnotations + .getOrDefault("unevaluatedProperties", Collections.emptyMap()).values().stream() + .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) + .collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } + + Set messages = new LinkedHashSet<>(); + for (Iterator it = node.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + if (!existingEvaluatedProperties.contains(fieldName)) { + evaluatedProperties.add(fieldName); + if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { + // All fails as "unevaluatedProperties: false" + messages.add(message().instanceLocation(instanceLocation.append(fieldName)) + .locale(executionContext.getExecutionConfig().getLocale()).build()); + } else { + messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, + instanceLocation.append(fieldName))); + } + } + } + if (!messages.isEmpty()) { + // Report these as unevaluated paths or not matching the unevaluatedProperties + // schema + messages = messages.stream() + .map(m -> message().instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(evaluatedProperties).build()); + + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; + + /** Set allPaths = allPaths(node, instanceLocation); // Short-circuit since schema is 'true' @@ -78,29 +180,30 @@ public Set validate(ExecutionContext executionContext, JsonNo } return Collections.emptySet(); + **/ } finally { collectorContext.enterDynamicScope(); } } - private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { - Set collector = new LinkedHashSet<>(); - node.fields().forEachRemaining(entry -> { - collector.add(instanceLocation.append(entry.getKey())); - }); - return collector; - } - - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - return unevaluatedPaths - .stream().map(path -> message().instanceLocation(path) - .locale(executionContext.getExecutionConfig().getLocale()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new LinkedHashSet<>(allPaths); - unevaluatedProperties.removeAll(collectorContext.getEvaluatedProperties()); - return unevaluatedProperties; - } +// private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { +// Set collector = new LinkedHashSet<>(); +// node.fields().forEachRemaining(entry -> { +// collector.add(instanceLocation.resolve(entry.getKey())); +// }); +// return collector; +// } +// +// private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { +// return unevaluatedPaths +// .stream().map(path -> message().instanceLocation(path) +// .locale(executionContext.getExecutionConfig().getLocale()).build()) +// .collect(Collectors.toCollection(LinkedHashSet::new)); +// } +// +// private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { +// Set unevaluatedProperties = new LinkedHashSet<>(allPaths); +// unevaluatedProperties.removeAll(collectorContext.getEvaluatedProperties()); +// return unevaluatedProperties; +// } } diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java new file mode 100644 index 000000000..457871044 --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.annotation; + +import java.util.Objects; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.Keyword; +import com.networknt.schema.SchemaLocation; + +/** + * The annotation. + */ +public class JsonNodeAnnotation { + private final String keyword; + private final JsonNodePath instanceLocation; + private final SchemaLocation schemaLocation; + private final JsonNodePath evaluationPath; + private final Object value; + + public JsonNodeAnnotation(String keyword, JsonNodePath instanceLocation, SchemaLocation schemaLocation, + JsonNodePath evaluationPath, Object value) { + super(); + this.keyword = keyword; + this.instanceLocation = instanceLocation; + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.value = value; + } + + /** + * The keyword that produces the annotation. + * + * @return the keyword + */ + public String getKeyword() { + return keyword; + } + + /** + * The instance location to which it is attached, as a JSON Pointer. + * + * @return the instance location + */ + public JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + /** + * The schema location of the attaching keyword, as a URI. + * + * @return the schema location + */ + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + /** + * The evaluation path, indicating how reference keywords such as "$ref" were + * followed to reach the absolute schema location. + * + * @return the evaluation path + */ + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + /** + * The attached value(s). + * + * @return the value + */ + @SuppressWarnings("unchecked") + public T getValue() { + return (T) value; + } + + @Override + public String toString() { + return "JsonNodeAnnotation [evaluationPath=" + evaluationPath + ", schemaLocation=" + schemaLocation + + ", instanceLocation=" + instanceLocation + ", keyword=" + keyword + ", value=" + value + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, keyword, schemaLocation, value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonNodeAnnotation other = (JsonNodeAnnotation) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) && Objects.equals(keyword, other.keyword) + && Objects.equals(schemaLocation, other.schemaLocation) && Objects.equals(value, other.value); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String keyword; + private JsonNodePath instanceLocation; + private SchemaLocation schemaLocation; + private JsonNodePath evaluationPath; + private Object value; + + public Builder keyword(Keyword keyword) { + this.keyword = keyword.getValue(); + return this; + } + + public Builder keyword(String keyword) { + this.keyword = keyword; + return this; + } + + public Builder instanceLocation(JsonNodePath instanceLocation) { + this.instanceLocation = instanceLocation; + return this; + } + + public Builder schemaLocation(SchemaLocation schemaLocation) { + this.schemaLocation = schemaLocation; + return this; + } + + public Builder evaluationPath(JsonNodePath evaluationPath) { + this.evaluationPath = evaluationPath; + return this; + } + + public Builder value(Object value) { + this.value = value; + return this; + } + + public JsonNodeAnnotation build() { + return new JsonNodeAnnotation(keyword, instanceLocation, schemaLocation, evaluationPath, value); + } + } + +} diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java new file mode 100644 index 000000000..5f356af12 --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.annotation; + +import java.util.function.Predicate; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * A predicate for filtering annotations. + */ +public class JsonNodeAnnotationPredicate implements Predicate { + final Predicate instanceLocationPredicate; + final Predicate evaluationPathPredicate; + final Predicate schemaLocationPredicate; + final Predicate keywordPredicate; + final Predicate valuePredicate; + + /** + * Initialize a new instance of this class. + * + * @param instanceLocationPredicate for instanceLocation + * @param evaluationPathPredicate for evaluationPath + * @param schemaLocationPredicate for schemaLocation + * @param keywordPredicate for keyword + * @param valuePredicate for value + */ + protected JsonNodeAnnotationPredicate(Predicate instanceLocationPredicate, + Predicate evaluationPathPredicate, Predicate schemaLocationPredicate, + Predicate keywordPredicate, Predicate valuePredicate) { + super(); + this.instanceLocationPredicate = instanceLocationPredicate; + this.evaluationPathPredicate = evaluationPathPredicate; + this.schemaLocationPredicate = schemaLocationPredicate; + this.keywordPredicate = keywordPredicate; + this.valuePredicate = valuePredicate; + } + + @Override + public boolean test(JsonNodeAnnotation t) { + return ((valuePredicate == null || valuePredicate.test(t.getValue())) + && (keywordPredicate == null || keywordPredicate.test(t.getKeyword())) + && (instanceLocationPredicate == null || instanceLocationPredicate.test(t.getInstanceLocation())) + && (evaluationPathPredicate == null || evaluationPathPredicate.test(t.getEvaluationPath())) + && (schemaLocationPredicate == null || schemaLocationPredicate.test(t.getSchemaLocation()))); + } + + /** + * Gets the predicate to filter on instanceLocation. + * + * @return the predicate + */ + public Predicate getInstanceLocationPredicate() { + return instanceLocationPredicate; + } + + /** + * Gets the predicate to filter on evaluationPath. + * + * @return the predicate + */ + public Predicate getEvaluationPathPredicate() { + return evaluationPathPredicate; + } + + /** + * Gets the predicate to filter on schemaLocation. + * + * @return the predicate + */ + public Predicate getSchemaLocationPredicate() { + return schemaLocationPredicate; + } + + /** + * Gets the predicate to filter on keyword. + * + * @return the predicate + */ + public Predicate getKeywordPredicate() { + return keywordPredicate; + } + + /** + * Gets the predicate to filter on value. + * + * @return the predicate + */ + public Predicate getValuePredicate() { + return valuePredicate; + } + + /** + * Creates a new builder to create the predicate. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for building a {@link JsonNodeAnnotationPredicate}. + */ + public static class Builder { + Predicate instanceLocationPredicate; + Predicate evaluationPathPredicate; + Predicate schemaLocationPredicate; + Predicate keywordPredicate; + Predicate valuePredicate; + + public Builder instanceLocation(Predicate instanceLocationPredicate) { + this.instanceLocationPredicate = instanceLocationPredicate; + return this; + } + + public Builder evaluationPath(Predicate evaluationPathPredicate) { + this.evaluationPathPredicate = evaluationPathPredicate; + return this; + } + + public Builder schema(Predicate schemaLocationPredicate) { + this.schemaLocationPredicate = schemaLocationPredicate; + return this; + } + + public Builder keyword(Predicate keywordPredicate) { + this.keywordPredicate = keywordPredicate; + return this; + } + + public Builder value(Predicate valuePredicate) { + this.valuePredicate = valuePredicate; + return this; + } + + public JsonNodeAnnotationPredicate build() { + return new JsonNodeAnnotationPredicate(instanceLocationPredicate, evaluationPathPredicate, + schemaLocationPredicate, keywordPredicate, valuePredicate); + } + } +} diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java new file mode 100644 index 000000000..318c44170 --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.annotation; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonNodePath; + +/** + * The JSON Schema annotations. + * + * @see Details + * of annotation collection + */ +public class JsonNodeAnnotations { + + public static class Stream { + private final Map>> annotations; + private final JsonNodeAnnotationPredicate filter; + + /** + * Initialize a new instance of this class. + * + * @param annotations the annotations + * @param filter the filter + */ + protected Stream(final Map>> annotations, + JsonNodeAnnotationPredicate filter) { + this.annotations = annotations; + this.filter = filter; + } + + /** + * Returns a stream that will match the filter. + * + * @param filter the filter + * @return a new stream + */ + public Stream filter(JsonNodeAnnotationPredicate filter) { + return new Stream(this.annotations, filter); + } + + /** + * Returns a stream that will match the filter. + * + * @param filter the filter + * @return a new stream + */ + public Stream filter(Consumer filter) { + JsonNodeAnnotationPredicate.Builder builder = JsonNodeAnnotationPredicate.builder(); + filter.accept(builder); + return filter(builder.build()); + } + + /** + * Performs an action for each element of this stream. + * + * @param action the action to be performed + */ + public void forEach(Consumer action) { + annotations.entrySet().stream().forEach(instances -> { + if (filter == null || filter.instanceLocationPredicate == null + || filter.instanceLocationPredicate.test(instances.getKey())) { + instances.getValue().entrySet().stream().forEach(keywords -> { + if (filter == null || filter.keywordPredicate == null + || filter.keywordPredicate.test(keywords.getKey())) { + keywords.getValue().entrySet().stream().forEach(evaluations -> { + if (filter == null || ((filter.evaluationPathPredicate == null + || filter.evaluationPathPredicate.test(evaluations.getKey())) + && (filter.valuePredicate == null + || filter.valuePredicate.test(evaluations.getValue().getValue())))) { + action.accept(evaluations.getValue()); + } + }); + } + }); + } + }); + } + + /** + * Collects the elements in the stream to a list. + * + * @return the list + */ + public List toList() { + List result = new ArrayList<>(); + forEach(result::add); + return result; + } + } + + /** + * Stores the annotations. + *

+ * instancePath -> keyword -> evaluationPath -> annotation + */ + private final Map>> values = new LinkedHashMap<>(); + + /** + * Gets the annotations. + *

+ * instancePath -> keyword -> evaluationPath -> annotation + * + * @return the annotations + */ + public Map>> asMap() { + return this.values; + } + + /** + * Puts the annotation. + * + * @param annotation the annotation + */ + public void put(JsonNodeAnnotation annotation) { + Map> instance = this.values + .computeIfAbsent(annotation.getInstanceLocation(), (key) -> new LinkedHashMap<>()); + Map keyword = instance.computeIfAbsent(annotation.getKeyword(), + (key) -> new LinkedHashMap<>()); + keyword.put(annotation.getEvaluationPath(), annotation); + + } + + /** + * Returns a stream for processing the annotations. + * + * @return the stream + */ + public Stream stream() { + return new Stream(values, null); + } + + @Override + public String toString() { + return Formatter.format(this.values); + } + + /** + * Formatter for pretty printing the annotations. + */ + public static class Formatter { + public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** + * Formats the annotations. + * + * @param annotations the annotations + * @return the formatted JSON + */ + public static String format(Map>> annotations) { + Map>> results = new LinkedHashMap<>(); + annotations.entrySet().stream().forEach(instances -> { + String instancePath = instances.getKey().toString(); + instances.getValue().entrySet().stream().forEach(keywords -> { + String keyword = keywords.getKey(); + keywords.getValue().entrySet().stream().forEach(evaluations -> { + String evaluationPath = evaluations.getKey().toString(); + Object annotation = evaluations.getValue().getValue(); + Map values = results + .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, annotation); + }); + }); + }); + + try { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); + } catch (JsonProcessingException e) { + return ""; + } + } + + } + +} diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java new file mode 100644 index 000000000..6703a3f19 --- /dev/null +++ b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.assertion; + +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.ValidationMessage; + +/** + * The JSON Schema assertions. + * + * @see Details + * of annotation collection + */ +public class JsonNodeAssertions { + + /** + * Stores the assertions. + *

+ * evaluationPath -> keyword -> instancePath -> assertion + */ + private final Map>> values = new LinkedHashMap<>(); + + /** + * Stores the assertions. + *

+ * evaluationPath -> keyword -> instancePath -> assertion + */ + public Map>> asMap() { + return this.values; + } + + /** + * Puts the assertion. + * + * @param assertion the assertion + */ + public void put(ValidationMessage assertion) { + Map> instance = this.values + .computeIfAbsent(assertion.getEvaluationPath(), (key) -> new LinkedHashMap<>()); + Map keyword = instance.computeIfAbsent(assertion.getType(), + (key) -> new LinkedHashMap<>()); + keyword.put(assertion.getInstanceLocation(), assertion); + + } + + @Override + public String toString() { + return Formatter.format(this.values); + } + + /** + * Formatter for pretty printing the assertions. + */ + public static class Formatter { + public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** + * Formats the assertions. + * + * @param assertions the assertions + * @return the formatted JSON + */ + public static String format(Map>> assertions) { + Map>> results = new LinkedHashMap<>(); + assertions.entrySet().stream().forEach(instances -> { + String instancePath = instances.getKey().toString(); + instances.getValue().entrySet().stream().forEach(keywords -> { + String keyword = keywords.getKey(); + keywords.getValue().entrySet().stream().forEach(evaluations -> { + String evaluationPath = evaluations.getKey().toString(); + Object assertion = evaluations.getValue().getMessage(); + Map values = results + .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, assertion); + }); + }); + }); + + try { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); + } catch (JsonProcessingException e) { + return ""; + } + } + + } + +} diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 374547c81..52819b4f4 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -42,7 +42,7 @@ propertyNames = Property name {0} is not valid for validation: {1} readOnly = {0}: is a readonly field, it cannot be changed required = {0}: required property ''{1}'' not found type = {0}: {1} found, {2} expected -unevaluatedItems = {0}: must not have unevaluated items +unevaluatedItems = {0}: must not have unevaluated items or must match unevaluated items schema unevaluatedProperties = {0}: must not have unevaluated properties unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique diff --git a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java new file mode 100644 index 000000000..5ce7fe523 --- /dev/null +++ b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.annotation.JsonNodeAnnotationPredicate; +import com.networknt.schema.annotation.JsonNodeAnnotations; + +class JsonNodeAnnotationsTest { + + @Test + void filter() { + JsonNodePath instanceRoot = new JsonNodePath(PathType.JSON_POINTER); + JsonNodePath instance = instanceRoot.append("foo"); + + JsonNodePath evaluationRoot = new JsonNodePath(PathType.JSON_POINTER).append("properties").append("foo"); + + JsonNodeAnnotations annotations = new JsonNodeAnnotations(); + + annotations.put(JsonNodeAnnotation.builder().keyword("title").instanceLocation(instance) + .evaluationPath(evaluationRoot.append("$ref").append("title")).value("Title in reference target") + .build()); + annotations.put(JsonNodeAnnotation.builder().keyword("title").instanceLocation(instance) + .evaluationPath(evaluationRoot.append("title")).value("Title adjacent to reference").build()); + annotations.put(JsonNodeAnnotation.builder().keyword("description").instanceLocation(instance) + .evaluationPath(evaluationRoot.append("$ref").append("description")).value("Even more text").build()); + annotations.put(JsonNodeAnnotation.builder().keyword("description").instanceLocation(instance) + .evaluationPath(evaluationRoot.append("description")).value("Lots of text").build()); + // 1 instance + assertEquals(1, annotations.asMap().size()); + + // 2 keywords + assertEquals(2, annotations.asMap().get(instance).size()); + + List all = annotations.stream().toList(); + assertEquals(4, all.size()); + + List titles = annotations.stream() + .filter(filter -> filter.keyword(keyword -> "title".equals(keyword))).toList(); + assertEquals(2, titles.size()); + + List allTitlesResult = all.stream() + .filter(JsonNodeAnnotationPredicate.builder().keyword(keyword -> "title".equals(keyword)).build()) + .collect(Collectors.toList()); + assertEquals(2, allTitlesResult.size()); + + } +} diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index dc7b2c56d..c46e58c9b 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -514,9 +514,7 @@ "valid": false, "validationMessages": [ "$.vehicle: required property 'wings' not found", - "$.vehicle.pontoons: must not have unevaluated properties", - "$.vehicle.unevaluated: must not have unevaluated properties", - "$.vehicle.wheels: must not have unevaluated properties" + "$.vehicle.unevaluated: must not have unevaluated properties" ] } ] From d222096401ba1d31653dbc348fc7c94a704fafca Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sat, 27 Jan 2024 09:09:14 +0800 Subject: [PATCH 02/53] Refactor recursive ref --- .../networknt/schema/DynamicRefValidator.java | 7 +- .../schema/RecursiveRefValidator.java | 112 +++++++++++++++--- 2 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index d2ce7a67a..30a288a78 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -32,7 +32,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,7 +85,6 @@ 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) { @@ -98,7 +97,7 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); JsonSchema refSchema = this.schema.getSchema(); if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.DYNAMIC_REF.getValue()) .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) .arguments(schemaNode.asText()).build(); @@ -128,7 +127,7 @@ public Set walk(ExecutionContext executionContext, JsonNode n // with the latest config. Reset the config. JsonSchema refSchema = this.schema.getSchema(); if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.DYNAMIC_REF.getValue()) .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) .arguments(schemaNode.asText()).build(); diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 16714be22..2a0401426 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -26,7 +26,7 @@ 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); @@ -39,32 +39,70 @@ public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluat .evaluationPath(schemaLocation.getFragment()).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<>(); + Set errors = Collections.emptySet(); 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); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = ValidationMessage.builder().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 JsonSchemaException(validationMessage); } + errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } finally { Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { parentScope.mergeWith(scope); } } - return errors; } @@ -72,19 +110,24 @@ public Set validate(ExecutionContext executionContext, JsonNo public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { CollectorContext collectorContext = executionContext.getCollectorContext(); - Set errors = new HashSet<>(); + Set errors = Collections.emptySet(); 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.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + // 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.RECURSIVE_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) { @@ -93,8 +136,39 @@ public Set walk(ExecutionContext executionContext, JsonNode n } } } + } - return errors; + public JsonSchemaRef getSchemaRef() { + return this.schema; } + + @Override + public void preloadJsonSchema() { + JsonSchema jsonSchema = null; + try { + 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; + } + } + if(!circularDependency) { + jsonSchema.initializeValidators(); + } + } } From 1012b32f91ef53cd153a286c8c243a5321a64135 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sat, 27 Jan 2024 09:09:03 +0800 Subject: [PATCH 03/53] Fix IllegalStateException on recursive call in computeIfAbsent --- .../networknt/schema/JsonSchemaFactory.java | 18 +++++++++++++++--- .../com/networknt/schema/RefValidator.java | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index f68d1d4e5..3656285c2 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -450,9 +450,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/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index e8c5016e1..d43e5279a 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -147,9 +147,22 @@ 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 From 3d1a3c1d53eda86496b9958382199aa9a2c03be9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:02:03 +0800 Subject: [PATCH 04/53] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 2 +- .../schema/PrefixItemsValidator.java | 1 - .../schema/UnevaluatedItemsValidator.java | 3 +- .../UnevaluatedPropertiesValidator.java | 5 +- .../schema/assertion/JsonNodeAssertions.java | 49 ++++++++----------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index d39f3de66..d28b5a1a2 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -517,7 +517,6 @@ public Set validate(ExecutionContext executionContext, JsonNo Scope parentScope = collectorContext.enterDynamicScope(this); try { results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); - results.forEach(executionContext.getAssertions()::put); } finally { Scope scope = collectorContext.exitDynamicScope(); if (results == null || results.isEmpty()) { @@ -527,6 +526,7 @@ public Set validate(ExecutionContext executionContext, JsonNo errors = new LinkedHashSet<>(); } errors.addAll(results); + executionContext.getAssertions().setValues(errors); // if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator // || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { // collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems()); diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 31ad2fbde..1429b1a0e 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -25,7 +25,6 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 5f07dc6f7..464b8cda1 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -80,7 +80,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Get all the valid adjacent annotations Predicate validEvaluationPathFilter = a -> { - for (JsonNodePath e : executionContext.getAssertions().asMap().keySet()) { + for (ValidationMessage assertion : executionContext.getAssertions().values()) { + JsonNodePath e = assertion.getEvaluationPath(); if (e.getParent().startsWith(a.getEvaluationPath()) || a.getEvaluationPath().startsWith(e.getParent())) { // Invalid diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 4cf0616ba..a4ffec779 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -52,10 +52,13 @@ public Set validate(ExecutionContext executionContext, JsonNo try { // Get all the valid adjacent annotations Predicate validEvaluationPathFilter = a -> { - for (JsonNodePath e : executionContext.getAssertions().asMap().keySet()) { + for (ValidationMessage assertion : executionContext.getAssertions().values()) { + JsonNodePath e = assertion.getEvaluationPath(); if (e.getParent().startsWith(a.getEvaluationPath()) || a.getEvaluationPath().startsWith(e.getParent())) { // Invalid + System.out.println(e.toString()); + System.out.println(a.getEvaluationPath().toString()); return false; } } diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java index 6703a3f19..ada51208a 100644 --- a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java +++ b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java @@ -15,12 +15,13 @@ */ package com.networknt.schema.assertion; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.JsonNodePath; import com.networknt.schema.ValidationMessage; /** @@ -34,17 +35,13 @@ public class JsonNodeAssertions { /** * Stores the assertions. - *

- * evaluationPath -> keyword -> instancePath -> assertion */ - private final Map>> values = new LinkedHashMap<>(); + private Set values = Collections.emptySet(); /** - * Stores the assertions. - *

- * evaluationPath -> keyword -> instancePath -> assertion + * Gets the assertions */ - public Map>> asMap() { + public Set values() { return this.values; } @@ -53,13 +50,12 @@ public Map>> asMa * * @param assertion the assertion */ - public void put(ValidationMessage assertion) { - Map> instance = this.values - .computeIfAbsent(assertion.getEvaluationPath(), (key) -> new LinkedHashMap<>()); - Map keyword = instance.computeIfAbsent(assertion.getType(), - (key) -> new LinkedHashMap<>()); - keyword.put(assertion.getInstanceLocation(), assertion); - + public void setValues(Set assertions) { + if (assertions != null) { + this.values = assertions; + } else { + this.values = Collections.emptySet(); + } } @Override @@ -79,21 +75,16 @@ public static class Formatter { * @param assertions the assertions * @return the formatted JSON */ - public static String format(Map>> assertions) { + public static String format(Set assertions) { Map>> results = new LinkedHashMap<>(); - assertions.entrySet().stream().forEach(instances -> { - String instancePath = instances.getKey().toString(); - instances.getValue().entrySet().stream().forEach(keywords -> { - String keyword = keywords.getKey(); - keywords.getValue().entrySet().stream().forEach(evaluations -> { - String evaluationPath = evaluations.getKey().toString(); - Object assertion = evaluations.getValue().getMessage(); - Map values = results - .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) - .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); - values.put(evaluationPath, assertion); - }); - }); + assertions.stream().forEach(assertion -> { + String instanceLocation = assertion.getInstanceLocation().toString(); + String keyword = assertion.getType(); + String evaluationPath = assertion.getEvaluationPath().toString(); + Object value = assertion.getMessage(); + Map values = results.computeIfAbsent(instanceLocation, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, value); }); try { From fcbb22e3a5eb50be861d61964f3d841d29d30c59 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 00:34:41 +0800 Subject: [PATCH 05/53] Refactor results --- .../networknt/schema/ExecutionContext.java | 11 ++- .../java/com/networknt/schema/JsonSchema.java | 3 +- .../schema/UnevaluatedItemsValidator.java | 10 +- .../UnevaluatedPropertiesValidator.java | 12 +-- .../schema/assertion/JsonNodeAssertions.java | 99 ------------------- .../schema/result/JsonNodeResult.java | 82 +++++++++++++++ .../schema/result/JsonNodeResults.java | 57 +++++++++++ 7 files changed, 149 insertions(+), 125 deletions(-) delete mode 100644 src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java create mode 100644 src/main/java/com/networknt/schema/result/JsonNodeResult.java create mode 100644 src/main/java/com/networknt/schema/result/JsonNodeResults.java diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index 662e9ae17..05becdd23 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -17,7 +17,8 @@ package com.networknt.schema; import com.networknt.schema.annotation.JsonNodeAnnotations; -import com.networknt.schema.assertion.JsonNodeAssertions; +import com.networknt.schema.result.JsonNodeResults; + import java.util.Stack; /** @@ -29,7 +30,7 @@ public class ExecutionContext { private ValidatorState validatorState = null; private Stack discriminatorContexts = new Stack<>(); private JsonNodeAnnotations annotations = new JsonNodeAnnotations(); - private JsonNodeAssertions assertions = new JsonNodeAssertions(); + private JsonNodeResults results = new JsonNodeResults(); /** * Creates an execution context. @@ -107,10 +108,10 @@ public JsonNodeAnnotations getAnnotations() { return annotations; } - public JsonNodeAssertions getAssertions() { - return assertions; + public JsonNodeResults getResults() { + return results; } - + /** * Gets the validator state. * diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index d28b5a1a2..9a9dfe171 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -520,13 +520,14 @@ public Set validate(ExecutionContext executionContext, JsonNo } finally { Scope scope = collectorContext.exitDynamicScope(); if (results == null || results.isEmpty()) { + executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), true); parentScope.mergeWith(scope); } else { + executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), false); if (errors == null) { errors = new LinkedHashSet<>(); } errors.addAll(results); - executionContext.getAssertions().setValues(errors); // if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator // || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { // collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems()); diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 464b8cda1..25c8a732d 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -80,15 +80,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // Get all the valid adjacent annotations Predicate validEvaluationPathFilter = a -> { - for (ValidationMessage assertion : executionContext.getAssertions().values()) { - JsonNodePath e = assertion.getEvaluationPath(); - if (e.getParent().startsWith(a.getEvaluationPath()) - || a.getEvaluationPath().startsWith(e.getParent())) { - // Invalid - return false; - } - } - return true; + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); }; Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index a4ffec779..fcc0fb4ef 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -52,17 +52,7 @@ public Set validate(ExecutionContext executionContext, JsonNo try { // Get all the valid adjacent annotations Predicate validEvaluationPathFilter = a -> { - for (ValidationMessage assertion : executionContext.getAssertions().values()) { - JsonNodePath e = assertion.getEvaluationPath(); - if (e.getParent().startsWith(a.getEvaluationPath()) - || a.getEvaluationPath().startsWith(e.getParent())) { - // Invalid - System.out.println(e.toString()); - System.out.println(a.getEvaluationPath().toString()); - return false; - } - } - return true; + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); }; Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java deleted file mode 100644 index ada51208a..000000000 --- a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.networknt.schema.assertion; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.ValidationMessage; - -/** - * The JSON Schema assertions. - * - * @see Details - * of annotation collection - */ -public class JsonNodeAssertions { - - /** - * Stores the assertions. - */ - private Set values = Collections.emptySet(); - - /** - * Gets the assertions - */ - public Set values() { - return this.values; - } - - /** - * Puts the assertion. - * - * @param assertion the assertion - */ - public void setValues(Set assertions) { - if (assertions != null) { - this.values = assertions; - } else { - this.values = Collections.emptySet(); - } - } - - @Override - public String toString() { - return Formatter.format(this.values); - } - - /** - * Formatter for pretty printing the assertions. - */ - public static class Formatter { - public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - /** - * Formats the assertions. - * - * @param assertions the assertions - * @return the formatted JSON - */ - public static String format(Set assertions) { - Map>> results = new LinkedHashMap<>(); - assertions.stream().forEach(assertion -> { - String instanceLocation = assertion.getInstanceLocation().toString(); - String keyword = assertion.getType(); - String evaluationPath = assertion.getEvaluationPath().toString(); - Object value = assertion.getMessage(); - Map values = results.computeIfAbsent(instanceLocation, (key) -> new LinkedHashMap<>()) - .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); - values.put(evaluationPath, value); - }); - - try { - return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); - } catch (JsonProcessingException e) { - return ""; - } - } - - } - -} diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResult.java b/src/main/java/com/networknt/schema/result/JsonNodeResult.java new file mode 100644 index 000000000..8b464d37b --- /dev/null +++ b/src/main/java/com/networknt/schema/result/JsonNodeResult.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.result; + +import java.util.Objects; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * Sub schema results. + */ +public class JsonNodeResult { + private final JsonNodePath instanceLocation; + private final SchemaLocation schemaLocation; + private final JsonNodePath evaluationPath; + private final boolean valid; + + public JsonNodeResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, + boolean valid) { + super(); + this.instanceLocation = instanceLocation; + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.valid = valid; + } + + public JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + public boolean isValid() { + return valid; + } + + @Override + public String toString() { + return "JsonNodeResult [instanceLocation=" + instanceLocation + ", schemaLocation=" + schemaLocation + + ", evaluationPath=" + evaluationPath + ", valid=" + valid + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, schemaLocation, valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonNodeResult other = (JsonNodeResult) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation) && valid == other.valid; + } + +} diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java new file mode 100644 index 000000000..c7ae5c46b --- /dev/null +++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.result; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * Sub schema results. + */ +public class JsonNodeResults { + + /** + * Stores the results. + */ + private Map> values = new HashMap<>(); + + public void setResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, + boolean valid) { + JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); + List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); + v.add(result); + } + + public boolean isValid(JsonNodePath instanceLocation, JsonNodePath evaluationPath) { + List instance = values.get(instanceLocation); + if (instance != null) { + for (JsonNodeResult result : instance) { + if (evaluationPath.startsWith(result.getEvaluationPath())) { + if(!result.isValid()) { + return false; + } + } + } + } + return true; + } + +} From d15d121cb995fa6b9081c1450f6970540b5c4dec Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 00:52:36 +0800 Subject: [PATCH 06/53] Refactor --- .../schema/UnevaluatedItemsValidator.java | 34 ++--- .../UnevaluatedPropertiesValidator.java | 16 ++- .../annotation/JsonNodeAnnotations.java | 127 +++--------------- .../schema/assertion/JsonNodeAssertions.java | 99 ++++++++++++++ .../schema/JsonNodeAnnotationsTest.java | 37 ----- 5 files changed, 143 insertions(+), 170 deletions(-) create mode 100644 src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 25c8a732d..44a61e279 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -59,17 +59,13 @@ public Set validate(ExecutionContext executionContext, JsonNo collectorContext.exitDynamicScope(); try { - String itemsKeyword = "items"; - String additionalItemsKeyword = "additionalItems"; - if (isMinV202012) { - /* - * Keywords renamed in 2020-12 - * - * items -> prefixItems additionalItems -> items - */ - itemsKeyword = "prefixItems"; - additionalItemsKeyword = "items"; - } + /* + * Keywords renamed in 2020-12 + * + * items -> prefixItems additionalItems -> items + */ + String itemsKeyword = isMinV202012 ? "prefixItems" : "items"; + String additionalItemsKeyword = isMinV202012 ? "items" : "additionalItems"; boolean valid = false; int validCount = 0; @@ -86,8 +82,8 @@ public Set validate(ExecutionContext executionContext, JsonNo Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() .startsWith(this.evaluationPath.getParent()); - Map> instanceLocationAnnotations = executionContext - .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyMap()); + List instanceLocationAnnotations = executionContext + .getAnnotations().getValues().getOrDefault(instanceLocation, Collections.emptyList()); // If schema is "unevaluatedItems: true" this is valid if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { @@ -100,7 +96,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } else { // Get all the "items" for the instanceLocation List items = instanceLocationAnnotations - .getOrDefault(itemsKeyword, Collections.emptyMap()).values().stream() + .stream() + .filter(a -> itemsKeyword.equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter) .filter(validEvaluationPathFilter) .collect(Collectors.toList()); @@ -134,7 +131,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // annotation value of true, then the combined result from these keywords is // also true. List additionalItems = instanceLocationAnnotations - .getOrDefault(additionalItemsKeyword, Collections.emptyMap()).values().stream() + .stream() + .filter(a -> additionalItemsKeyword.equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter) .filter(validEvaluationPathFilter) .collect(Collectors.toList()); @@ -150,7 +148,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Unevaluated // Check if there are any "unevaluatedItems" annotations List unevaluatedItems = instanceLocationAnnotations - .getOrDefault("unevaluatedItems", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "unevaluatedItems".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter) .filter(validEvaluationPathFilter) .collect(Collectors.toList()); @@ -166,7 +165,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!valid) { // Get all the "contains" for the instanceLocation List contains = instanceLocationAnnotations - .getOrDefault("contains", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "contains".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter) .collect(Collectors.toList()); diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index fcc0fb4ef..32eee12d2 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -58,14 +58,15 @@ public Set validate(ExecutionContext executionContext, JsonNo Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() .startsWith(this.evaluationPath.getParent()); - Map> instanceLocationAnnotations = executionContext - .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyMap()); + List instanceLocationAnnotations = executionContext + .getAnnotations().getValues().getOrDefault(instanceLocation, Collections.emptyList()); Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema Set existingEvaluatedProperties = new LinkedHashSet<>(); // Get all the "properties" for the instanceLocation List properties = instanceLocationAnnotations - .getOrDefault("properties", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "properties".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) .collect(Collectors.toList()); for (JsonNodeAnnotation annotation : properties) { @@ -77,7 +78,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Get all the "patternProperties" for the instanceLocation List patternProperties = instanceLocationAnnotations - .getOrDefault("patternProperties", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "patternProperties".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) .collect(Collectors.toList()); for (JsonNodeAnnotation annotation : patternProperties) { @@ -89,7 +91,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Get all the "patternProperties" for the instanceLocation List additionalProperties = instanceLocationAnnotations - .getOrDefault("additionalProperties", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "additionalProperties".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) .collect(Collectors.toList()); for (JsonNodeAnnotation annotation : additionalProperties) { @@ -101,7 +104,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Get all the "unevaluatedProperties" for the instanceLocation List unevaluatedProperties = instanceLocationAnnotations - .getOrDefault("unevaluatedProperties", Collections.emptyMap()).values().stream() + .stream() + .filter(a -> "unevaluatedProperties".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) .collect(Collectors.toList()); for (JsonNodeAnnotation annotation : unevaluatedProperties) { diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java index 318c44170..23c103a58 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -19,7 +19,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Consumer; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -34,97 +33,21 @@ */ public class JsonNodeAnnotations { - public static class Stream { - private final Map>> annotations; - private final JsonNodeAnnotationPredicate filter; - - /** - * Initialize a new instance of this class. - * - * @param annotations the annotations - * @param filter the filter - */ - protected Stream(final Map>> annotations, - JsonNodeAnnotationPredicate filter) { - this.annotations = annotations; - this.filter = filter; - } - - /** - * Returns a stream that will match the filter. - * - * @param filter the filter - * @return a new stream - */ - public Stream filter(JsonNodeAnnotationPredicate filter) { - return new Stream(this.annotations, filter); - } - - /** - * Returns a stream that will match the filter. - * - * @param filter the filter - * @return a new stream - */ - public Stream filter(Consumer filter) { - JsonNodeAnnotationPredicate.Builder builder = JsonNodeAnnotationPredicate.builder(); - filter.accept(builder); - return filter(builder.build()); - } - - /** - * Performs an action for each element of this stream. - * - * @param action the action to be performed - */ - public void forEach(Consumer action) { - annotations.entrySet().stream().forEach(instances -> { - if (filter == null || filter.instanceLocationPredicate == null - || filter.instanceLocationPredicate.test(instances.getKey())) { - instances.getValue().entrySet().stream().forEach(keywords -> { - if (filter == null || filter.keywordPredicate == null - || filter.keywordPredicate.test(keywords.getKey())) { - keywords.getValue().entrySet().stream().forEach(evaluations -> { - if (filter == null || ((filter.evaluationPathPredicate == null - || filter.evaluationPathPredicate.test(evaluations.getKey())) - && (filter.valuePredicate == null - || filter.valuePredicate.test(evaluations.getValue().getValue())))) { - action.accept(evaluations.getValue()); - } - }); - } - }); - } - }); - } - - /** - * Collects the elements in the stream to a list. - * - * @return the list - */ - public List toList() { - List result = new ArrayList<>(); - forEach(result::add); - return result; - } - } - /** * Stores the annotations. *

- * instancePath -> keyword -> evaluationPath -> annotation + * instancePath -> annotation */ - private final Map>> values = new LinkedHashMap<>(); + private final Map> values = new LinkedHashMap<>(); /** * Gets the annotations. *

- * instancePath -> keyword -> evaluationPath -> annotation + * instancePath -> annotation * * @return the annotations */ - public Map>> asMap() { + public Map> getValues() { return this.values; } @@ -134,23 +57,10 @@ public Map>> asM * @param annotation the annotation */ public void put(JsonNodeAnnotation annotation) { - Map> instance = this.values - .computeIfAbsent(annotation.getInstanceLocation(), (key) -> new LinkedHashMap<>()); - Map keyword = instance.computeIfAbsent(annotation.getKeyword(), - (key) -> new LinkedHashMap<>()); - keyword.put(annotation.getEvaluationPath(), annotation); + this.values.computeIfAbsent(annotation.getInstanceLocation(), (k) -> new ArrayList<>()).add(annotation); } - /** - * Returns a stream for processing the annotations. - * - * @return the stream - */ - public Stream stream() { - return new Stream(values, null); - } - @Override public String toString() { return Formatter.format(this.values); @@ -168,22 +78,19 @@ public static class Formatter { * @param annotations the annotations * @return the formatted JSON */ - public static String format(Map>> annotations) { + public static String format(Map> v) { Map>> results = new LinkedHashMap<>(); - annotations.entrySet().stream().forEach(instances -> { - String instancePath = instances.getKey().toString(); - instances.getValue().entrySet().stream().forEach(keywords -> { - String keyword = keywords.getKey(); - keywords.getValue().entrySet().stream().forEach(evaluations -> { - String evaluationPath = evaluations.getKey().toString(); - Object annotation = evaluations.getValue().getValue(); - Map values = results - .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) - .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); - values.put(evaluationPath, annotation); - }); - }); - }); + for (List annotations : v.values()) { + for (JsonNodeAnnotation annotation : annotations) { + String keyword = annotation.getKeyword(); + String instancePath = annotation.getInstanceLocation().toString(); + String evaluationPath = annotation.getEvaluationPath().toString(); + Map values = results + .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, annotation); + } + } try { return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java new file mode 100644 index 000000000..ada51208a --- /dev/null +++ b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.assertion; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.ValidationMessage; + +/** + * The JSON Schema assertions. + * + * @see Details + * of annotation collection + */ +public class JsonNodeAssertions { + + /** + * Stores the assertions. + */ + private Set values = Collections.emptySet(); + + /** + * Gets the assertions + */ + public Set values() { + return this.values; + } + + /** + * Puts the assertion. + * + * @param assertion the assertion + */ + public void setValues(Set assertions) { + if (assertions != null) { + this.values = assertions; + } else { + this.values = Collections.emptySet(); + } + } + + @Override + public String toString() { + return Formatter.format(this.values); + } + + /** + * Formatter for pretty printing the assertions. + */ + public static class Formatter { + public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** + * Formats the assertions. + * + * @param assertions the assertions + * @return the formatted JSON + */ + public static String format(Set assertions) { + Map>> results = new LinkedHashMap<>(); + assertions.stream().forEach(assertion -> { + String instanceLocation = assertion.getInstanceLocation().toString(); + String keyword = assertion.getType(); + String evaluationPath = assertion.getEvaluationPath().toString(); + Object value = assertion.getMessage(); + Map values = results.computeIfAbsent(instanceLocation, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, value); + }); + + try { + return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); + } catch (JsonProcessingException e) { + return ""; + } + } + + } + +} diff --git a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java index 5ce7fe523..091d69342 100644 --- a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java +++ b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java @@ -28,41 +28,4 @@ class JsonNodeAnnotationsTest { - @Test - void filter() { - JsonNodePath instanceRoot = new JsonNodePath(PathType.JSON_POINTER); - JsonNodePath instance = instanceRoot.append("foo"); - - JsonNodePath evaluationRoot = new JsonNodePath(PathType.JSON_POINTER).append("properties").append("foo"); - - JsonNodeAnnotations annotations = new JsonNodeAnnotations(); - - annotations.put(JsonNodeAnnotation.builder().keyword("title").instanceLocation(instance) - .evaluationPath(evaluationRoot.append("$ref").append("title")).value("Title in reference target") - .build()); - annotations.put(JsonNodeAnnotation.builder().keyword("title").instanceLocation(instance) - .evaluationPath(evaluationRoot.append("title")).value("Title adjacent to reference").build()); - annotations.put(JsonNodeAnnotation.builder().keyword("description").instanceLocation(instance) - .evaluationPath(evaluationRoot.append("$ref").append("description")).value("Even more text").build()); - annotations.put(JsonNodeAnnotation.builder().keyword("description").instanceLocation(instance) - .evaluationPath(evaluationRoot.append("description")).value("Lots of text").build()); - // 1 instance - assertEquals(1, annotations.asMap().size()); - - // 2 keywords - assertEquals(2, annotations.asMap().get(instance).size()); - - List all = annotations.stream().toList(); - assertEquals(4, all.size()); - - List titles = annotations.stream() - .filter(filter -> filter.keyword(keyword -> "title".equals(keyword))).toList(); - assertEquals(2, titles.size()); - - List allTitlesResult = all.stream() - .filter(JsonNodeAnnotationPredicate.builder().keyword(keyword -> "title".equals(keyword)).build()) - .collect(Collectors.toList()); - assertEquals(2, allTitlesResult.size()); - - } } From b42afe9d49ef70e3ef7b952a2663dd754a1c801f Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 05:33:26 +0800 Subject: [PATCH 07/53] Remove scope --- .../com/networknt/schema/AllOfValidator.java | 6 - .../com/networknt/schema/AnyOfValidator.java | 10 -- .../networknt/schema/CollectorContext.java | 151 ------------------ .../networknt/schema/DynamicRefValidator.java | 16 -- .../com/networknt/schema/IfValidator.java | 10 -- .../java/com/networknt/schema/JsonSchema.java | 30 +--- .../com/networknt/schema/NotValidator.java | 7 - .../com/networknt/schema/OneOfValidator.java | 13 -- .../schema/RecursiveRefValidator.java | 13 -- .../com/networknt/schema/RefValidator.java | 19 --- .../schema/UnevaluatedItemsValidator.java | 68 -------- .../UnevaluatedPropertiesValidator.java | 59 ------- 12 files changed, 1 insertion(+), 401 deletions(-) diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index cf1dc2bc0..1f9ee0b7d 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +51,6 @@ public Set validate(ExecutionContext executionContext, JsonNo for (JsonSchema schema : this.schemas) { Set localErrors = new HashSet<>(); - Scope parentScope = collectorContext.enterDynamicScope(); try { if (!state.isWalkEnabled()) { localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); @@ -94,10 +92,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (localErrors.isEmpty()) { - parentScope.mergeWith(scope); - } } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index df735200f..66d8f5dc0 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,12 +62,10 @@ public Set validate(ExecutionContext executionContext, JsonNo Set allErrors = new LinkedHashSet<>(); - Scope grandParentScope = collectorContext.enterDynamicScope(); try { int numberOfValidSubSchemas = 0; for (JsonSchema schema: this.schemas) { Set errors = Collections.emptySet(); - Scope parentScope = collectorContext.enterDynamicScope(); try { state.setMatchedNode(initialHasMatchedNode); @@ -119,10 +116,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } allErrors.addAll(errors); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } } @@ -149,11 +142,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { executionContext.leaveDiscriminatorContextImmediately(instanceLocation); } - - Scope parentScope = collectorContext.exitDynamicScope(); if (allErrors.isEmpty()) { state.setMatchedNode(true); - grandParentScope.mergeWith(parentScope); } } return Collections.unmodifiableSet(allErrors); diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 59feef82b..2469fceda 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -15,14 +15,7 @@ */ package com.networknt.schema; -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -42,73 +35,9 @@ public class CollectorContext { */ private Map collectorLoadMap = new HashMap<>(); - private final Deque dynamicScopes = new LinkedList<>(); - public CollectorContext() { - this(false, false); - } - - public CollectorContext(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties) { - this.dynamicScopes.push(newTopScope()); - } - - /** - * Creates a new scope - * @return the previous, parent scope - */ - public Scope enterDynamicScope() { - return enterDynamicScope(null); - } - - /** - * Creates a new scope - * - * @param containingSchema the containing schema - * @return the previous, parent scope - */ - public Scope enterDynamicScope(JsonSchema containingSchema) { - Scope parent = this.dynamicScopes.peek(); - this.dynamicScopes.push(newScope(null != containingSchema ? containingSchema : parent.getContainingSchema())); - return parent; - } - - /** - * Restores the previous, parent scope - * @return the exited scope - */ - public Scope exitDynamicScope() { - return this.dynamicScopes.pop(); } - /** - * Provides the currently active scope - * @return the active scope - */ - public Scope getDynamicScope() { - return this.dynamicScopes.peek(); - } - - public JsonSchema getOutermostSchema() { - - JsonSchema context = getDynamicScope().getContainingSchema(); - if (null == context) { - throw new IllegalStateException("Missing a root schema in the dynamic scope."); - } - - JsonSchema lexicalRoot = context.findLexicalRoot(); - if (lexicalRoot.isRecursiveAnchor()) { - Iterator it = this.dynamicScopes.descendingIterator(); - while (it.hasNext()) { - Scope scope = it.next(); - JsonSchema containingSchema = scope.getContainingSchema(); - if (null != containingSchema && containingSchema.isRecursiveAnchor()) { - return containingSchema; - } - } - } - - return context.findLexicalRoot(); - } /** * Adds a collector with give name. Preserving this method for backward @@ -200,85 +129,5 @@ void loadCollectors() { this.collectorLoadMap.put(entry.getKey(), collector.collect()); } } - - } - - private Scope newScope(JsonSchema containingSchema) { - return new Scope(containingSchema); - } - - private Scope newTopScope() { - return new Scope(true, null); - } - - public static class Scope { - - private final JsonSchema containingSchema; - - private final boolean top; - - Scope(JsonSchema containingSchema) { - this(false, containingSchema); - } - - Scope(boolean top, JsonSchema containingSchema) { - this.top = top; - this.containingSchema = containingSchema; - } - - private static Collection newCollection(boolean disabled) { - return !disabled ? new ArrayList<>() : new AbstractCollection() { - - @Override - public boolean add(JsonNodePath e) { - return false; - } - - @Override - public Iterator iterator() { - return Collections.emptyIterator(); - } - - @Override - public boolean remove(Object o) { - return false; - } - - @Override - public int size() { - return 0; - } - - }; - } - - public boolean isTop() { - return this.top; - } - - public JsonSchema getContainingSchema() { - return this.containingSchema; - } - - /** - * Merges the provided scope into this scope. - * @param scope the scope to merge - * @return this scope - */ - public Scope mergeWith(Scope scope) { -// getEvaluatedItems().addAll(scope.getEvaluatedItems()); -// getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); - return this; - } - -// @Override -// public String toString() { -// return new StringBuilder("{ ") -// .append("\"evaluatedItems\": ").append(this.evaluatedItems) -// .append(", ") -// .append("\"evaluatedProperties\": ").append(this.evaluatedProperties) -// .append(" }").toString(); -// } - } } diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index 30a288a78..4d4cfaf1d 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,11 +87,9 @@ private static String resolve(JsonSchema parentSchema, String 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(); @@ -105,21 +102,14 @@ public Set validate(ExecutionContext executionContext, JsonNo } errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } return errors; } @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, @@ -136,12 +126,6 @@ public Set walk(ExecutionContext executionContext, JsonNode n errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); return errors; } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } - } } } diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 0d90912ff..e9ed16ddd 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +67,6 @@ public Set validate(ExecutionContext executionContext, JsonNo Set errors = new LinkedHashSet<>(); - Scope parentScope = collectorContext.enterDynamicScope(); boolean ifConditionPassed = false; try { try { @@ -82,18 +80,10 @@ public Set validate(ExecutionContext executionContext, JsonNo 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)); } } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 9a9dfe171..0686aed10 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; @@ -506,38 +505,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()) { executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), true); - parentScope.mergeWith(scope); } 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()); -// } } - } } @@ -958,7 +943,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(); @@ -970,23 +954,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()); -// } } } } @@ -1081,8 +1054,7 @@ 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(); diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 6e5164301..c6f8f1203 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,10 +35,8 @@ 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<>(); - Scope parentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, instanceLocation); errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); @@ -50,10 +47,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } return Collections.emptySet(); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 2e1fea253..86bc6ba04 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,7 +42,6 @@ public Set validate(ExecutionContext executionContext, JsonNo Set errors = new LinkedHashSet<>(); CollectorContext collectorContext = executionContext.getCollectorContext(); - Scope grandParentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, instanceLocation); @@ -58,7 +56,6 @@ public Set validate(ExecutionContext executionContext, JsonNo 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); @@ -85,10 +82,6 @@ public Set validate(ExecutionContext executionContext, JsonNo childErrors.addAll(schemaErrors); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (schemaErrors.isEmpty()) { - parentScope.mergeWith(scope); - } } } @@ -102,8 +95,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } errors.add(message); errors.addAll(childErrors); -// collectorContext.getEvaluatedItems().clear(); -// collectorContext.getEvaluatedProperties().clear(); } // Make sure to signal parent handlers we matched @@ -115,10 +106,6 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.unmodifiableSet(errors); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - grandParentScope.mergeWith(scope); - } } } diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 2a0401426..19ea13554 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -85,7 +84,6 @@ public Set validate(ExecutionContext executionContext, JsonNo Set errors = Collections.emptySet(); - Scope parentScope = collectorContext.enterDynamicScope(); try { debug(logger, node, rootNode, instanceLocation); JsonSchema refSchema = this.schema.getSchema(); @@ -98,10 +96,6 @@ public Set validate(ExecutionContext executionContext, JsonNo } errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } return errors; } @@ -112,7 +106,6 @@ public Set walk(ExecutionContext executionContext, JsonNode n 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, @@ -129,12 +122,6 @@ public Set walk(ExecutionContext executionContext, JsonNode n errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); return errors; } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } - } } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index d43e5279a..88942b85c 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -167,11 +166,7 @@ private static JsonSchema getJsonSchema(JsonSchema parent, @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(); @@ -184,21 +179,13 @@ public Set validate(ExecutionContext executionContext, JsonNo } errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } return errors; } @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, @@ -215,12 +202,6 @@ public Set walk(ExecutionContext executionContext, JsonNode n errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); return errors; } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } - } } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 44a61e279..b0cfb7996 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -55,9 +55,6 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isArray()) return Collections.emptySet(); debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); - - collectorContext.exitDynamicScope(); try { /* * Keywords renamed in 2020-12 @@ -216,72 +213,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .keyword("unevaluatedItems").value(true).build()); } return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; - /* - if (!valid) { - System.out.println(instanceLocation + " " + node); - throw new IllegalArgumentException(instanceLocation + " " + node); - }*/ - -/* - Set allPaths = allPaths(node, instanceLocation); - - // Short-circuit since schema is 'true' - if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { - collectorContext.getEvaluatedItems().addAll(allPaths); - return Collections.emptySet(); - } - - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); - - // Short-circuit since schema is 'false' - if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths, executionContext); - } - - Set failingPaths = new LinkedHashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = path.getPathType().convertToJsonPointer(path.toString()); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { - failingPaths.add(path); - } - }); - - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(allPaths); - } else { - return reportUnevaluatedPaths(failingPaths, executionContext); - } - - return Collections.emptySet(); - */ } finally { - collectorContext.enterDynamicScope(); } } - -// private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { -// Set collector = new LinkedHashSet<>(); -// int size = node.size(); -// for (int i = 0; i < size; ++i) { -// JsonNodePath path = instanceLocation.resolve(i); -// collector.add(path); -// } -// return collector; -// } -// -// private Set reportUnevaluatedPaths(Set unevaluatedPaths, -// ExecutionContext executionContext) { -// return unevaluatedPaths -// .stream().map(path -> message().instanceLocation(path) -// .locale(executionContext.getExecutionConfig().getLocale()).build()) -// .collect(Collectors.toCollection(LinkedHashSet::new)); -// } -// -// private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { -// Set unevaluatedProperties = new HashSet<>(allPaths); -// unevaluatedProperties.removeAll(collectorContext.getEvaluatedItems()); -// return unevaluatedProperties; -// } - } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 32eee12d2..8886644aa 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -46,9 +46,6 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!node.isObject()) return Collections.emptySet(); debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); - - collectorContext.exitDynamicScope(); try { // Get all the valid adjacent annotations Predicate validEvaluationPathFilter = a -> { @@ -144,63 +141,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .keyword(getKeyword()).value(evaluatedProperties).build()); return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; - - /** - Set allPaths = allPaths(node, instanceLocation); - - // Short-circuit since schema is 'true' - if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { - collectorContext.getEvaluatedProperties().addAll(allPaths); - return Collections.emptySet(); - } - - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); - - // Short-circuit since schema is 'false' - if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths, executionContext); - } - - Set failingPaths = new LinkedHashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = path.getPathType().convertToJsonPointer(path.toString()); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { - failingPaths.add(path); - } - }); - - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedProperties().addAll(allPaths); - } else { - return reportUnevaluatedPaths(failingPaths, executionContext); - } - - return Collections.emptySet(); - **/ } finally { - collectorContext.enterDynamicScope(); } } - -// private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { -// Set collector = new LinkedHashSet<>(); -// node.fields().forEachRemaining(entry -> { -// collector.add(instanceLocation.resolve(entry.getKey())); -// }); -// return collector; -// } -// -// private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { -// return unevaluatedPaths -// .stream().map(path -> message().instanceLocation(path) -// .locale(executionContext.getExecutionConfig().getLocale()).build()) -// .collect(Collectors.toCollection(LinkedHashSet::new)); -// } -// -// private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { -// Set unevaluatedProperties = new LinkedHashSet<>(allPaths); -// unevaluatedProperties.removeAll(collectorContext.getEvaluatedProperties()); -// return unevaluatedProperties; -// } } From cbb084eaef20caac5960c6c7ed0d1a21acc52b5a Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 05:39:12 +0800 Subject: [PATCH 08/53] Refactor --- .../networknt/schema/annotation/JsonNodeAnnotations.java | 6 +++--- .../com/networknt/schema/assertion/JsonNodeAssertions.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java index 23c103a58..8f3621245 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -78,10 +78,10 @@ public static class Formatter { * @param annotations the annotations * @return the formatted JSON */ - public static String format(Map> v) { + public static String format(Map> annotations) { Map>> results = new LinkedHashMap<>(); - for (List annotations : v.values()) { - for (JsonNodeAnnotation annotation : annotations) { + for (List list : annotations.values()) { + for (JsonNodeAnnotation annotation : list) { String keyword = annotation.getKeyword(); String instancePath = annotation.getInstanceLocation().toString(); String evaluationPath = annotation.getEvaluationPath().toString(); diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java index ada51208a..99dfb6285 100644 --- a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java +++ b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java @@ -48,7 +48,7 @@ public Set values() { /** * Puts the assertion. * - * @param assertion the assertion + * @param assertions the assertion */ public void setValues(Set assertions) { if (assertions != null) { From 3611966c04733b80799df40e28e664e81f7b3bce Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 10:13:08 +0800 Subject: [PATCH 09/53] Add can short circuit logic --- .../com/networknt/schema/AnyOfValidator.java | 24 +++- .../com/networknt/schema/OneOfValidator.java | 19 +++- .../schema/RecursiveRefValidator.java | 4 - .../unevaluatedTests/unevaluated-tests.json | 3 +- src/test/suite/tests/draft2019-09/not.json | 4 +- .../tests/draft2019-09/unevaluatedItems.json | 84 +++++++++++++- .../draft2019-09/unevaluatedProperties.json | 104 +++++++++++++++++- 7 files changed, 216 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 66d8f5dc0..a2693633a 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -31,6 +31,8 @@ public class AnyOfValidator extends BaseJsonValidator { private final List schemas = new ArrayList<>(); private final DiscriminatorContext discriminatorContext; + private Boolean canShortCircuit = null; + public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); int size = schemaNode.size(); @@ -49,7 +51,6 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); @@ -62,8 +63,8 @@ public Set validate(ExecutionContext executionContext, JsonNo Set allErrors = new LinkedHashSet<>(); + int numberOfValidSubSchemas = 0; try { - int numberOfValidSubSchemas = 0; for (JsonSchema schema: this.schemas) { Set errors = Collections.emptySet(); try { @@ -95,7 +96,7 @@ public Set validate(ExecutionContext executionContext, JsonNo numberOfValidSubSchemas++; } - if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) { + if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) && canShortCircuit()) { // Clear all errors. allErrors.clear(); // return empty errors. @@ -146,6 +147,9 @@ public Set validate(ExecutionContext executionContext, JsonNo state.setMatchedNode(true); } } + if (numberOfValidSubSchemas >= 1) { + return Collections.emptySet(); + } return Collections.unmodifiableSet(allErrors); } @@ -159,6 +163,20 @@ public Set walk(ExecutionContext executionContext, JsonNode n } return new LinkedHashSet<>(); } + + 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; + } @Override public void preloadJsonSchema() { diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 86bc6ba04..bb59e0f1d 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -28,6 +28,8 @@ public class OneOfValidator extends BaseJsonValidator { 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(); @@ -40,7 +42,6 @@ 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(); try { debug(logger, node, rootNode, instanceLocation); @@ -75,7 +76,7 @@ public Set validate(ExecutionContext executionContext, JsonNo numberOfValidSchema++; } - if (numberOfValidSchema > 1) { + if (numberOfValidSchema > 1 && canShortCircuit()) { // short-circuit break; } @@ -108,6 +109,20 @@ public Set validate(ExecutionContext executionContext, JsonNo } finally { } } + + 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) { ValidatorState state = executionContext.getValidatorState(); diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 19ea13554..744136e3a 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -80,8 +80,6 @@ static JsonSchema getSchema(JsonSchema parentSchema, ValidationContext validatio @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - Set errors = Collections.emptySet(); try { @@ -102,8 +100,6 @@ public Set validate(ExecutionContext executionContext, JsonNo @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - Set errors = Collections.emptySet(); try { diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index c46e58c9b..b5b3baab2 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -415,8 +415,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.unevaluated: must not have unevaluated properties", - "$.vehicle.wings: must not have unevaluated properties" + "$.vehicle.unevaluated: must not have unevaluated properties" ] } ] diff --git a/src/test/suite/tests/draft2019-09/not.json b/src/test/suite/tests/draft2019-09/not.json index af4df4c7a..62c9af9de 100644 --- a/src/test/suite/tests/draft2019-09/not.json +++ b/src/test/suite/tests/draft2019-09/not.json @@ -146,9 +146,7 @@ { "description": "annotations are still collected inside a 'not'", "data": { "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: Annotations are not supported; only assertions are supported" + "valid": false } ] } diff --git a/src/test/suite/tests/draft2019-09/unevaluatedItems.json b/src/test/suite/tests/draft2019-09/unevaluatedItems.json index dc0615e09..8e2ee4b11 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedItems.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedItems.json @@ -212,9 +212,7 @@ { "description": "with invalid additional item", "data": ["yes", false], - "valid": false, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against 'false'." + "valid": false } ] }, @@ -310,9 +308,7 @@ { "description": "when two schemas match and has no unevaluated items", "data": ["foo", "bar", "baz"], - "valid": true, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against a string." + "valid": true }, { "description": "when two schemas match and has unevaluated items", @@ -484,6 +480,82 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "unevaluatedItems": false, + "items": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "items": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/unevaluated-items-with-recursive-ref/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "./tree", + "items": [ + true, + true, + { "type": "string" } + ], + + "$defs": { + "tree": { + "$id": "./tree", + "$recursiveAnchor": true, + + "type": "array", + "items": [ + { "type": "number" }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [1, [2, [], "b"], "a"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": [1, [2, [], "b", "too many"], "a"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json index d75eab341..71c36dfa0 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json @@ -358,9 +358,7 @@ "bar": "bar", "baz": "baz" }, - "valid": true, - "disabled": true, - "reason": "TODO: AnyOfValidator is short-circuiting" + "valid": true }, { "description": "when two match and has unevaluated properties", @@ -717,6 +715,102 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/unevaluated-properties-with-recursive-ref/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "./tree", + "properties": { + "name": { "type": "string" } + }, + + "$defs": { + "tree": { + "$id": "./tree", + "$recursiveAnchor": true, + + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": ["node"] + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "name": "b", + "node": 2 + } + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "foo": "b", + "node": 2 + } + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { @@ -1328,9 +1422,7 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: unevaluatedProperties is not correct" + "valid": false }, { "description": "xx + a is invalid", From 5d38dd0cb72d8ceb49bca3f419c88c97eb1383b7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:24:47 +0800 Subject: [PATCH 10/53] Fix performance --- .../schema/AdditionalPropertiesValidator.java | 29 ++++-- .../com/networknt/schema/AllOfValidator.java | 1 - .../networknt/schema/BaseJsonValidator.java | 22 +++++ .../networknt/schema/ContainsValidator.java | 96 ++++++++++-------- .../com/networknt/schema/IfValidator.java | 2 - .../com/networknt/schema/ItemsValidator.java | 63 +++++++----- .../schema/ItemsValidator202012.java | 26 ++++- .../schema/PatternPropertiesValidator.java | 25 ++++- .../schema/PrefixItemsValidator.java | 46 ++++++--- .../networknt/schema/PropertiesValidator.java | 24 ++++- .../schema/result/JsonNodeResults.java | 8 +- .../draft2019-09/unevaluatedProperties.json | 4 +- src/test/suite/tests/draft2020-12/not.json | 4 +- .../tests/draft2020-12/unevaluatedItems.json | 88 +++++++++++++++-- .../draft2020-12/unevaluatedProperties.json | 97 ++++++++++++++++++- 15 files changed, 414 insertions(+), 121 deletions(-) diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index e64b31632..c94ef8088 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -33,6 +33,8 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator { private final Set allowedProperties; private final List patternProperties = new ArrayList<>(); + private Boolean hasUnevaluatedPropertiesValidator; + public AdditionalPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES, validationContext); @@ -73,14 +75,11 @@ public Set validate(ExecutionContext executionContext, JsonNo } Set matchedInstancePropertyNames = new LinkedHashSet<>(); - - CollectorContext collectorContext = executionContext.getCollectorContext(); // if allowAdditionalProperties is true, add all the properties as evaluated. if (allowAdditionalProperties) { for (Iterator it = node.fieldNames(); it.hasNext();) { String fieldName = it.next(); matchedInstancePropertyNames.add(fieldName); -// collectorContext.getEvaluatedProperties().add(instanceLocation.resolve(fieldName)); } } @@ -131,10 +130,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) - .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(matchedInstancePropertyNames) - .build()); + 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); } @@ -178,10 +179,22 @@ public Set walk(ExecutionContext executionContext, JsonNode n return Collections.emptySet(); } + private boolean collectAnnotations() { + return hasUnevaluatedPropertiesValidator(); + } + + private boolean hasUnevaluatedPropertiesValidator() { + if (this.hasUnevaluatedPropertiesValidator == null) { + this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties"); + } + return hasUnevaluatedPropertiesValidator; + } + @Override public void preloadJsonSchema() { - if(additionalPropertiesSchema!=null) { + if(additionalPropertiesSchema != null) { additionalPropertiesSchema.initializeValidators(); + collectAnnotations(); } } } diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 1f9ee0b7d..e6586d7ce 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -41,7 +41,6 @@ public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 8a30144fa..29693e8b6 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -346,4 +346,26 @@ protected JsonNodePath atRoot() { public String toString() { return getEvaluationPath().getName(-1); } + + protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { + boolean hasValidator = validationContext.getMetaSchema().getKeywords() + .get(keyword) != null; + if (hasValidator) { + JsonSchema schema = getEvaluationParentSchema(); + while (schema != null) { + for (JsonValidator validator : schema.getValidators()) { + if (keyword.equals(validator.getKeyword())) { + hasValidator = true; + break; + } + } + if (hasValidator) { + break; + } + schema = schema.getEvaluationParentSchema(); + } + } + return hasValidator; + } + } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index dd4a193df..860e44110 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -24,7 +24,6 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -44,6 +43,8 @@ public class ContainsValidator extends BaseJsonValidator { private Integer min = null; private Integer max = null; + + private Boolean hasUnevaluatedItemsValidator = null; public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext); @@ -77,13 +78,10 @@ public Set validate(ExecutionContext executionContext, JsonNo int actual = 0, i = 0; List indexes = new ArrayList<>(); // for the annotation if (null != this.schema && node.isArray()) { -// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); - for (JsonNode n : node) { JsonNodePath path = instanceLocation.append(i); if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { ++actual; -// evaluatedItems.add(path); indexes.add(i); } ++i; @@ -109,44 +107,46 @@ public Set validate(ExecutionContext executionContext, JsonNo } } - if (this.schema != null) { - // This keyword produces an annotation value which is an array of the indexes to - // which this keyword validates successfully when applying its subschema, in - // ascending order. The value MAY be a boolean "true" if the subschema validates - // successfully when applied to every index of the instance. The annotation MUST - // be present if the instance array to which this keyword's schema applies is - // empty. - if (actual == i) { - // evaluated all - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(true).build()); - } else { - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(indexes).build()); - } - // Add minContains and maxContains annotations - if (this.min != null) { - // Omitted keywords MUST NOT produce annotation results. However, as described - // in the section for contains, the absence of this keyword's annotation causes - // contains to assume a minimum value of 1. - String minContainsKeyword = "minContains"; - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath.append(minContainsKeyword)) - .schemaLocation(this.schemaLocation.append(minContainsKeyword)) - .keyword(minContainsKeyword).value(this.min).build()); - } - if (this.max != null) { - String maxContainsKeyword = "maxContains"; - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath.append(maxContainsKeyword)) - .schemaLocation(this.schemaLocation.append(maxContainsKeyword)) - .keyword(maxContainsKeyword).value(this.max).build()); + if (collectAnnotations()) { + if (this.schema != null) { + // This keyword produces an annotation value which is an array of the indexes to + // which this keyword validates successfully when applying its subschema, in + // ascending order. The value MAY be a boolean "true" if the subschema validates + // successfully when applied to every index of the instance. The annotation MUST + // be present if the instance array to which this keyword's schema applies is + // empty. + if (actual == i) { + // evaluated all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } else { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(indexes).build()); + } + // Add minContains and maxContains annotations + if (this.min != null) { + // Omitted keywords MUST NOT produce annotation results. However, as described + // in the section for contains, the absence of this keyword's annotation causes + // contains to assume a minimum value of 1. + String minContainsKeyword = "minContains"; + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(minContainsKeyword)) + .schemaLocation(this.schemaLocation.append(minContainsKeyword)) + .keyword(minContainsKeyword).value(this.min).build()); + } + if (this.max != null) { + String maxContainsKeyword = "maxContains"; + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(maxContainsKeyword)) + .schemaLocation(this.schemaLocation.append(maxContainsKeyword)) + .keyword(maxContainsKeyword).value(this.max).build()); + } } } return results == null ? Collections.emptySet() : results; @@ -155,10 +155,22 @@ public Set validate(ExecutionContext executionContext, JsonNo @Override public void preloadJsonSchema() { Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators); + collectAnnotations(); } 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 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/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index e9ed16ddd..42bfdb99d 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -63,8 +63,6 @@ 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<>(); boolean ifConditionPassed = false; diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 53fd0fc5e..c57095d5d 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -37,6 +37,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); @@ -81,28 +83,30 @@ public Set validate(ExecutionContext executionContext, JsonNo } // Add items annotation - 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 { + if (collectAnnotations()) { + 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()); + } } } @@ -123,10 +127,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (hasAdditionalItem) { - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword("additionalItems").value(true).build()); + if (collectAnnotations()) { + 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); } @@ -178,8 +184,6 @@ private boolean doValidate(ExecutionContext executionContext, Set 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() { if (null != this.schema) { this.schema.initializeValidators(); + collectAnnotations(); } preloadJsonSchemas(this.tupleSchema); if (null != this.additionalSchema) { this.additionalSchema.initializeValidators(); + collectAnnotations(); } } } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 2873eee01..70dec2350 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -34,6 +34,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); @@ -76,11 +78,13 @@ public Set validate(ExecutionContext executionContext, JsonNo evaluated = true; } if (evaluated) { - // Applies to all - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(true).build()); + if (collectAnnotations()) { + // 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 { @@ -153,6 +157,18 @@ public JsonSchema getSchema() { @Override public void preloadJsonSchema() { this.schema.initializeValidators(); + collectAnnotations(); + } + + 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/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index eb6af8a34..35a01c11c 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -29,6 +29,8 @@ public class PatternPropertiesValidator extends BaseJsonValidator { 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); @@ -62,7 +64,6 @@ public Set validate(ExecutionContext executionContext, JsonNo Set results = entry.getValue().validate(executionContext, n, rootNode, path); if (results.isEmpty()) { matchedInstancePropertyNames.add(name); -// executionContext.getCollectorContext().getEvaluatedProperties().add(path); } else { if (errors == null) { errors = new LinkedHashSet<>(); @@ -72,15 +73,29 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) - .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(matchedInstancePropertyNames) - .build()); + 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(); } } diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 1429b1a0e..c062b1a06 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -35,6 +35,8 @@ public class PrefixItemsValidator extends BaseJsonValidator { 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); @@ -71,21 +73,23 @@ public Set validate(ExecutionContext executionContext, JsonNo } // Add annotation - // 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()); + if (collectAnnotations()) { + // 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 { @@ -156,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(); } } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 964f8e861..95a26c868 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -31,6 +31,8 @@ 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); @@ -118,10 +120,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) - .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(matchedInstancePropertyNames) - .build()); + if (collectAnnotations()) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(matchedInstancePropertyNames).build()); + } return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @@ -143,6 +147,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()); @@ -192,5 +207,6 @@ public Map getSchemas() { @Override public void preloadJsonSchema() { preloadJsonSchemas(this.schemas.values()); + collectAnnotations(); } } diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java index c7ae5c46b..cdfda4f02 100644 --- a/src/main/java/com/networknt/schema/result/JsonNodeResults.java +++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java @@ -35,9 +35,11 @@ public class JsonNodeResults { public void setResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, boolean valid) { - JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); - List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); - v.add(result); + if (!valid) { + JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); + List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); + v.add(result); + } } public boolean isValid(JsonNodePath instanceLocation, JsonNodePath evaluationPath) { diff --git a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json index 71c36dfa0..21372359f 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json @@ -1422,7 +1422,9 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false + "valid": false, + "disabled": true, + "reason": "TODO: unevaluatedProperties is not correct" }, { "description": "xx + a is invalid", diff --git a/src/test/suite/tests/draft2020-12/not.json b/src/test/suite/tests/draft2020-12/not.json index 5d5148277..57e45ba39 100644 --- a/src/test/suite/tests/draft2020-12/not.json +++ b/src/test/suite/tests/draft2020-12/not.json @@ -146,9 +146,7 @@ { "description": "annotations are still collected inside a 'not'", "data": { "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: Annotations are not supported; only assertions are supported" + "valid": false } ] } diff --git a/src/test/suite/tests/draft2020-12/unevaluatedItems.json b/src/test/suite/tests/draft2020-12/unevaluatedItems.json index 722b5dc03..ee0cb6586 100644 --- a/src/test/suite/tests/draft2020-12/unevaluatedItems.json +++ b/src/test/suite/tests/draft2020-12/unevaluatedItems.json @@ -191,9 +191,7 @@ { "description": "with invalid additional item", "data": ["yes", false], - "valid": false, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against 'false'." + "valid": false } ] }, @@ -289,9 +287,7 @@ { "description": "when two schemas match and has no unevaluated items", "data": ["foo", "bar", "baz"], - "valid": true, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against a string." + "valid": true }, { "description": "when two schemas match and has unevaluated items", @@ -465,6 +461,86 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "unevaluatedItems": false, + "prefixItems": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "prefixItems": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/unevaluated-items-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json index 83987ae6d..c88a65a03 100644 --- a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json @@ -358,9 +358,7 @@ "bar": "bar", "baz": "baz" }, - "valid": true, - "disabled": true, - "reason": "TODO: AnyOfValidator is short-circuiting" + "valid": true }, { "description": "when two match and has unevaluated properties", @@ -717,6 +715,99 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/unevaluated-properties-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { From 560bf2626fe70400a664bd5c3323f561cfe48884 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 11:39:13 +0800 Subject: [PATCH 11/53] Fix --- .../java/com/networknt/schema/result/JsonNodeResults.java | 2 +- src/test/suite/tests/draft2019-09/unevaluatedProperties.json | 4 +--- src/test/suite/tests/draft2020-12/unevaluatedProperties.json | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java index cdfda4f02..b6d8e8932 100644 --- a/src/main/java/com/networknt/schema/result/JsonNodeResults.java +++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java @@ -46,7 +46,7 @@ public boolean isValid(JsonNodePath instanceLocation, JsonNodePath evaluationPat List instance = values.get(instanceLocation); if (instance != null) { for (JsonNodeResult result : instance) { - if (evaluationPath.startsWith(result.getEvaluationPath())) { + if (evaluationPath.startsWith(result.getEvaluationPath().getParent())) { if(!result.isValid()) { return false; } diff --git a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json index 21372359f..71c36dfa0 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json @@ -1422,9 +1422,7 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: unevaluatedProperties is not correct" + "valid": false }, { "description": "xx + a is invalid", diff --git a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json index c88a65a03..b8a2306ca 100644 --- a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json @@ -1419,9 +1419,7 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: unevaluatedProperties is not correct" + "valid": false }, { "description": "xx + a is invalid", From cc1b49deaa5eb0cb4aafbcbabe5f3d6355136395 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 20:10:53 +0800 Subject: [PATCH 12/53] Refactor --- .../networknt/schema/ContainsValidator.java | 4 +- .../com/networknt/schema/ItemsValidator.java | 13 +-- .../java/com/networknt/schema/JsonSchema.java | 2 +- .../networknt/schema/PropertiesValidator.java | 17 +++- .../schema/UnevaluatedItemsValidator.java | 7 +- .../UnevaluatedPropertiesValidator.java | 6 +- .../schema/annotation/JsonNodeAnnotation.java | 3 +- .../annotation/JsonNodeAnnotations.java | 10 +- .../schema/assertion/JsonNodeAssertions.java | 99 ------------------- .../schema/result/JsonNodeResults.java | 10 +- .../schema/JsonNodeAnnotationsTest.java | 21 ++-- 11 files changed, 50 insertions(+), 142 deletions(-) delete mode 100644 src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 860e44110..39529b0e6 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.annotation.JsonNodeAnnotation; import org.slf4j.Logger; @@ -36,7 +35,6 @@ public class ContainsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ContainsValidator.class); private static final String CONTAINS_MAX = "contains.max"; private static final String CONTAINS_MIN = "contains.min"; - private static final VersionFlag DEFAULT_VERSION = VersionFlag.V6; private final JsonSchema schema; private final boolean isMinV201909; @@ -52,7 +50,7 @@ public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP // Draft 6 added the contains keyword but maxContains and minContains first // appeared in Draft 2019-09 so the semantics of the validation changes // slightly. - isMinV201909 = MinV201909.getVersions().contains(SpecVersionDetector.detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION)); + this.isMinV201909 = MinV201909.getVersions().contains(this.validationContext.getMetaSchema().getSpecification()); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index c57095d5d..4436794f5 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -140,25 +140,20 @@ public Set validate(ExecutionContext executionContext, JsonNo private boolean doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { boolean isAdditionalItem = false; -// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); JsonNodePath path = instanceLocation.append(i); if (this.schema != null) { // validate with item schema (the whole array has the same item // schema) Set results = this.schema.validate(executionContext, node, rootNode, path); - if (results.isEmpty()) { -// evaluatedItems.add(path); - } 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()) { -// evaluatedItems.add(path); - } else { + if (!results.isEmpty()) { errors.addAll(results); } } else { @@ -169,9 +164,7 @@ private boolean doValidate(ExecutionContext executionContext, Set results = this.additionalSchema.validate(executionContext, node, rootNode, path); - if (results.isEmpty()) { -// evaluatedItems.add(path); - } else { + if (!results.isEmpty()) { errors.addAll(results); } } else if (this.additionalItems != null) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 0686aed10..3c5955689 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -515,7 +515,7 @@ public Set validate(ExecutionContext executionContext, JsonNo results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); } finally { if (results == null || results.isEmpty()) { - executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), true); + // Do nothing if valid } else { executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), false); if (errors == null) { diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 95a26c868..9fb0ac7b8 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -55,14 +55,19 @@ public Set validate(ExecutionContext executionContext, JsonNo ValidatorState state = executionContext.getValidatorState(); Set requiredErrors = null; - Set matchedInstancePropertyNames = new LinkedHashSet<>(); + Set matchedInstancePropertyNames = null; + boolean collectAnnotations = collectAnnotations(); 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()); -// collectorContext.getEvaluatedProperties().add(path); // TODO: This should happen after validation - matchedInstancePropertyNames.add(entry.getKey()); + 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(); // if this is a complex validator, the node has matched, and all it's child elements, if available, are to be validated @@ -120,11 +125,13 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } - if (collectAnnotations()) { + if (collectAnnotations) { executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(matchedInstancePropertyNames).build()); + .keyword(getKeyword()).value(matchedInstancePropertyNames == null ? Collections.emptySet() + : matchedInstancePropertyNames) + .build()); } return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index b0cfb7996..51460b75e 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -52,7 +52,9 @@ public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath eva @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isArray()) return Collections.emptySet(); + if (!node.isArray()) { + return Collections.emptySet(); + } debug(logger, node, rootNode, instanceLocation); try { @@ -80,7 +82,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .startsWith(this.evaluationPath.getParent()); List instanceLocationAnnotations = executionContext - .getAnnotations().getValues().getOrDefault(instanceLocation, Collections.emptyList()); + .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyList()); // If schema is "unevaluatedItems: true" this is valid if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { @@ -165,6 +167,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .stream() .filter(a -> "contains".equals(a.getKeyword())) .filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter) .collect(Collectors.toList()); Set containsEvaluated = new HashSet<>(); diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 8886644aa..053ab585d 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -43,7 +43,9 @@ public UnevaluatedPropertiesValidator(SchemaLocation schemaLocation, JsonNodePat @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!node.isObject()) return Collections.emptySet(); + if (!node.isObject()) { + return Collections.emptySet(); + } debug(logger, node, rootNode, instanceLocation); try { @@ -56,7 +58,7 @@ public Set validate(ExecutionContext executionContext, JsonNo .startsWith(this.evaluationPath.getParent()); List instanceLocationAnnotations = executionContext - .getAnnotations().getValues().getOrDefault(instanceLocation, Collections.emptyList()); + .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyList()); Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema Set existingEvaluatedProperties = new LinkedHashSet<>(); diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java index 457871044..e717aa9a7 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java @@ -60,7 +60,8 @@ public JsonNodePath getInstanceLocation() { } /** - * The schema location of the attaching keyword, as a URI. + * The schema location of the attaching keyword, as a IRI and JSON Pointer + * fragment. * * @return the schema location */ diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java index 8f3621245..2c371a678 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -21,8 +21,8 @@ import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonNodePath; +import com.networknt.schema.serialization.JsonMapperFactory; /** * The JSON Schema annotations. @@ -47,7 +47,7 @@ public class JsonNodeAnnotations { * * @return the annotations */ - public Map> getValues() { + public Map> asMap() { return this.values; } @@ -70,8 +70,6 @@ public String toString() { * Formatter for pretty printing the annotations. */ public static class Formatter { - public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - /** * Formats the annotations. * @@ -88,12 +86,12 @@ public static String format(Map> annotati Map values = results .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); - values.put(evaluationPath, annotation); + values.put(evaluationPath, annotation.getValue()); } } try { - return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(results); } catch (JsonProcessingException e) { return ""; } diff --git a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java b/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java deleted file mode 100644 index 99dfb6285..000000000 --- a/src/main/java/com/networknt/schema/assertion/JsonNodeAssertions.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.networknt.schema.assertion; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.ValidationMessage; - -/** - * The JSON Schema assertions. - * - * @see Details - * of annotation collection - */ -public class JsonNodeAssertions { - - /** - * Stores the assertions. - */ - private Set values = Collections.emptySet(); - - /** - * Gets the assertions - */ - public Set values() { - return this.values; - } - - /** - * Puts the assertion. - * - * @param assertions the assertion - */ - public void setValues(Set assertions) { - if (assertions != null) { - this.values = assertions; - } else { - this.values = Collections.emptySet(); - } - } - - @Override - public String toString() { - return Formatter.format(this.values); - } - - /** - * Formatter for pretty printing the assertions. - */ - public static class Formatter { - public static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - /** - * Formats the assertions. - * - * @param assertions the assertions - * @return the formatted JSON - */ - public static String format(Set assertions) { - Map>> results = new LinkedHashMap<>(); - assertions.stream().forEach(assertion -> { - String instanceLocation = assertion.getInstanceLocation().toString(); - String keyword = assertion.getType(); - String evaluationPath = assertion.getEvaluationPath().toString(); - Object value = assertion.getMessage(); - Map values = results.computeIfAbsent(instanceLocation, (key) -> new LinkedHashMap<>()) - .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); - values.put(evaluationPath, value); - }); - - try { - return OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(results); - } catch (JsonProcessingException e) { - return ""; - } - } - - } - -} diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java index b6d8e8932..07a419441 100644 --- a/src/main/java/com/networknt/schema/result/JsonNodeResults.java +++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java @@ -29,17 +29,15 @@ public class JsonNodeResults { /** - * Stores the results. + * Stores the invalid results. */ private Map> values = new HashMap<>(); public void setResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, boolean valid) { - if (!valid) { - JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); - List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); - v.add(result); - } + JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); + List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); + v.add(result); } public boolean isValid(JsonNodePath instanceLocation, JsonNodePath evaluationPath) { diff --git a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java index 091d69342..596393985 100644 --- a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java +++ b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 the original author or authors. + * 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. @@ -15,17 +15,24 @@ */ package com.networknt.schema; -import static org.junit.jupiter.api.Assertions.*; - -import java.util.List; -import java.util.stream.Collectors; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import com.networknt.schema.annotation.JsonNodeAnnotation; -import com.networknt.schema.annotation.JsonNodeAnnotationPredicate; import com.networknt.schema.annotation.JsonNodeAnnotations; +/** + * JsonNodeAnnotationsTest. + */ class JsonNodeAnnotationsTest { - + @Test + void put() { + JsonNodeAnnotations annotations = new JsonNodeAnnotations(); + JsonNodeAnnotation annotation = new JsonNodeAnnotation("unevaluatedProperties", + new JsonNodePath(PathType.JSON_POINTER), SchemaLocation.of(""), new JsonNodePath(PathType.JSON_POINTER), + "test"); + annotations.put(annotation); + assertTrue(annotations.asMap().get(annotation.getInstanceLocation()).contains(annotation)); + } } From 6c1d6559cd6eb00d70cbefdaf4aa4cd224d19b6c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Sun, 28 Jan 2024 20:52:55 +0800 Subject: [PATCH 13/53] Enable disabled tests --- .../draft2019-09/optional/format/iri.json | 4 +-- .../tests/draft2019-09/recursiveRef.json | 4 --- src/test/suite/tests/draft2019-09/ref.json | 26 ------------------- .../suite/tests/draft2019-09/refRemote.json | 2 -- .../draft2020-12/optional/format/iri.json | 4 +-- src/test/suite/tests/draft2020-12/ref.json | 26 ------------------- .../suite/tests/draft2020-12/refRemote.json | 2 -- src/test/suite/tests/draft4/ref.json | 8 ------ src/test/suite/tests/draft4/refRemote.json | 2 -- src/test/suite/tests/draft6/ref.json | 18 ------------- src/test/suite/tests/draft6/refRemote.json | 2 -- src/test/suite/tests/draft7/ref.json | 26 ------------------- src/test/suite/tests/draft7/refRemote.json | 2 -- 13 files changed, 2 insertions(+), 124 deletions(-) diff --git a/src/test/suite/tests/draft2019-09/optional/format/iri.json b/src/test/suite/tests/draft2019-09/optional/format/iri.json index 808c3c1eb..ad4c79e83 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/iri.json +++ b/src/test/suite/tests/draft2019-09/optional/format/iri.json @@ -64,9 +64,7 @@ { "description": "an invalid IRI based on IPv6", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": false, - "disabled": true, - "reason": "URI syntax cannot always distinguish a malformed server-based authority from a legitimate registry-based authority" + "valid": false }, { "description": "an invalid relative IRI Reference", diff --git a/src/test/suite/tests/draft2019-09/recursiveRef.json b/src/test/suite/tests/draft2019-09/recursiveRef.json index 600b4a74d..22b47e749 100644 --- a/src/test/suite/tests/draft2019-09/recursiveRef.json +++ b/src/test/suite/tests/draft2019-09/recursiveRef.json @@ -348,8 +348,6 @@ "$ref": "recursiveRef8_inner.json" } }, - "disabled": true, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "recurse to anyLeafNode - floats are allowed", @@ -394,8 +392,6 @@ "$ref": "main.json#/$defs/inner" } }, - "disabled": true, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "numeric node", diff --git a/src/test/suite/tests/draft2019-09/ref.json b/src/test/suite/tests/draft2019-09/ref.json index 95a73345d..6d3a5cbe4 100644 --- a/src/test/suite/tests/draft2019-09/ref.json +++ b/src/test/suite/tests/draft2019-09/ref.json @@ -309,8 +309,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -475,8 +473,6 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -530,8 +526,6 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -589,8 +583,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -624,8 +616,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "data is valid against first definition", @@ -684,8 +674,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -830,8 +818,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -860,8 +846,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -887,8 +871,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -911,8 +893,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -935,8 +915,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -959,8 +937,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -990,8 +966,6 @@ }, "$ref": "/absref/foobar.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft2019-09/refRemote.json b/src/test/suite/tests/draft2019-09/refRemote.json index 00bf60b5b..79107f9e4 100644 --- a/src/test/suite/tests/draft2019-09/refRemote.json +++ b/src/test/suite/tests/draft2019-09/refRemote.json @@ -113,8 +113,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft2020-12/optional/format/iri.json b/src/test/suite/tests/draft2020-12/optional/format/iri.json index 4b91f154e..311c9ef08 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/iri.json +++ b/src/test/suite/tests/draft2020-12/optional/format/iri.json @@ -64,9 +64,7 @@ { "description": "an invalid IRI based on IPv6", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": false, - "disabled": true, - "reason": "URI syntax cannot always distinguish a malformed server-based authority from a legitimate registry-based authority" + "valid": false }, { "description": "an invalid relative IRI Reference", diff --git a/src/test/suite/tests/draft2020-12/ref.json b/src/test/suite/tests/draft2020-12/ref.json index 7ceb50e6e..5f6be8c20 100644 --- a/src/test/suite/tests/draft2020-12/ref.json +++ b/src/test/suite/tests/draft2020-12/ref.json @@ -309,8 +309,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -475,8 +473,6 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -530,8 +526,6 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -589,8 +583,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -624,8 +616,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "data is valid against first definition", @@ -684,8 +674,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -830,8 +818,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -860,8 +846,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -887,8 +871,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -911,8 +893,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -935,8 +915,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -959,8 +937,6 @@ "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -990,8 +966,6 @@ }, "$ref": "/absref/foobar.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft2020-12/refRemote.json b/src/test/suite/tests/draft2020-12/refRemote.json index 17c36a29a..f4a5b1bfb 100644 --- a/src/test/suite/tests/draft2020-12/refRemote.json +++ b/src/test/suite/tests/draft2020-12/refRemote.json @@ -113,8 +113,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft4/ref.json b/src/test/suite/tests/draft4/ref.json index 4b170eb34..b53bd2abe 100644 --- a/src/test/suite/tests/draft4/ref.json +++ b/src/test/suite/tests/draft4/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -301,8 +299,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -436,8 +432,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -497,8 +491,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft4/refRemote.json b/src/test/suite/tests/draft4/refRemote.json index fb1d03cfe..412c9ff83 100644 --- a/src/test/suite/tests/draft4/refRemote.json +++ b/src/test/suite/tests/draft4/refRemote.json @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft6/ref.json b/src/test/suite/tests/draft6/ref.json index ed9fe56a5..8a8908a44 100644 --- a/src/test/suite/tests/draft6/ref.json +++ b/src/test/suite/tests/draft6/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -333,8 +331,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -468,8 +464,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -530,8 +524,6 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -584,8 +576,6 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -629,8 +619,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -755,8 +743,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -784,8 +770,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -817,8 +801,6 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft6/refRemote.json b/src/test/suite/tests/draft6/refRemote.json index 22baff6d3..c2b200249 100644 --- a/src/test/suite/tests/draft6/refRemote.json +++ b/src/test/suite/tests/draft6/refRemote.json @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft7/ref.json b/src/test/suite/tests/draft7/ref.json index 82c1e8c24..82631726e 100644 --- a/src/test/suite/tests/draft7/ref.json +++ b/src/test/suite/tests/draft7/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -333,8 +331,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -468,8 +464,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -530,8 +524,6 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -584,8 +576,6 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -642,8 +632,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -667,8 +655,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -793,8 +779,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -822,8 +806,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -850,8 +832,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -878,8 +858,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -906,8 +884,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -939,8 +915,6 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft7/refRemote.json b/src/test/suite/tests/draft7/refRemote.json index 22baff6d3..c2b200249 100644 --- a/src/test/suite/tests/draft7/refRemote.json +++ b/src/test/suite/tests/draft7/refRemote.json @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", From 4bc4eaa9c9d010e6b00fa0873dd1a0e471497ea1 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:21:39 +0800 Subject: [PATCH 14/53] Fix contains --- .../networknt/schema/ContainsValidator.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 39529b0e6..14bd7075f 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -89,18 +89,12 @@ public Set validate(ExecutionContext executionContext, JsonNo m = this.min; } if (actual < m) { - if(isMinV201909) { - updateValidatorType(ValidatorTypeCode.MIN_CONTAINS); - } - results = boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), + results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MIN_CONTAINS : ValidatorTypeCode.CONTAINS, executionContext.getExecutionConfig().getLocale(), instanceLocation, m); } if (this.max != null && actual > this.max) { - if(isMinV201909) { - updateValidatorType(ValidatorTypeCode.MAX_CONTAINS); - } - results = boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), + results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MAX_CONTAINS : ValidatorTypeCode.CONTAINS, executionContext.getExecutionConfig().getLocale(), instanceLocation, this.max); } } @@ -118,12 +112,12 @@ public Set validate(ExecutionContext executionContext, JsonNo executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(true).build()); + .keyword("contains").value(true).build()); } else { executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(indexes).build()); + .keyword("contains").value(indexes).build()); } // Add minContains and maxContains annotations if (this.min != null) { @@ -156,9 +150,18 @@ public void preloadJsonSchema() { collectAnnotations(); } - 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 Set boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, + 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().instanceLocation(instanceLocation).messageKey(messageKey) + .locale(locale).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()) + .code(validatorTypeCode.getErrorCode()).type(validatorTypeCode.getValue()).build()); } private boolean collectAnnotations() { From b4e2d9a661baf1fa403892ab71cd9277684b250b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 13:30:19 +0800 Subject: [PATCH 15/53] Fix fast fail --- .../com/networknt/schema/JsonNodePath.java | 23 ++++++ .../schema/ValidationMessageHandler.java | 75 ++++++++++++------- 2 files changed, 72 insertions(+), 26 deletions(-) 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/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 663c036c2..61c3260b6 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -58,7 +58,7 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { protected MessageSourceValidationMessage.Builder message() { return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, message -> { - if (this.failFast && isApplicator()) { + if (canFastFail()) { throw new JsonSchemaException(message); } }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) @@ -70,40 +70,63 @@ protected ErrorMessageType getErrorMessageType() { return this.errorMessageType; } - private boolean isApplicator() { - return !isPartOfAnyOfMultipleType() - && !isPartOfIfMultipleType() - && !isPartOfNotMultipleType() - && !isPartOfOneOfMultipleType(); + /** + * Determines if the evaluation can fast fail. + * + * @return true if it can fast fail + */ + protected boolean canFastFail() { + return this.failFast && !hasApplicatorInEvaluationPath(); } - private boolean isPartOfAnyOfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.ANY_OF.getValue()); + /** + * Determines if there is an applicator in the evaluation path for determining + * if it is possible to fast fail. + *

+ * For instance if there is a not keyword in the evaluation path this can change + * the overall result. + * + * @return true if there is an applicator in the evaluation path + */ + private boolean hasApplicatorInEvaluationPath() { + return hasAnyOfInEvaluationPath() || hasIfInEvaluationPath() || hasNotInEvaluationPath() + || hasOneOfInEvaluationPath(); } - private boolean isPartOfIfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.IF_THEN_ELSE.getValue()); + /** + * Determines if anyOf is in the evaluation path. + * + * @return true if anyOf is in the evaluation path + */ + private boolean hasAnyOfInEvaluationPath() { + return this.evaluationPath.contains(ValidatorTypeCode.ANY_OF.getValue()); } - private boolean isPartOfNotMultipleType() { - return schemaLocationContains(ValidatorTypeCode.NOT.getValue()); - } - - protected boolean schemaLocationContains(String match) { - int count = this.parentSchema.schemaLocation.getFragment().getNameCount(); - for (int x = 0; x < count; x++) { - String name = this.parentSchema.schemaLocation.getFragment().getName(x); - if (match.equals(name)) { - return true; - } - } - return false; + /** + * Determines if if is in the evaluation path. + * + * @return true if if is in the evaluation path + */ + private boolean hasIfInEvaluationPath() { + return this.evaluationPath.contains(ValidatorTypeCode.IF_THEN_ELSE.getValue()); } - /* ********************** START OF OpenAPI 3.0.x DISCRIMINATOR METHODS ********************************* */ + /** + * Determines if not is in the evaluation path. + * + * @return true if not is in the evaluation path + */ + private boolean hasNotInEvaluationPath() { + return this.evaluationPath.contains(ValidatorTypeCode.NOT.getValue()); + } - protected boolean isPartOfOneOfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.ONE_OF.getValue()); + /** + * Determines if oneOf is in the evaluation path. + * + * @return true if oneOf is in the evaluation path + */ + protected boolean hasOneOfInEvaluationPath() { + return this.evaluationPath.contains(ValidatorTypeCode.ONE_OF.getValue()); } protected void parseErrorCode(String errorCodeKey) { From 00763c51c728399c74fb10806018c9f7e4de322a Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:31:37 +0800 Subject: [PATCH 16/53] Refactor --- src/main/java/com/networknt/schema/TypeValidator.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 3e4e62f08..a4b40a1c6 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -61,17 +61,6 @@ public Set validate(ExecutionContext executionContext, JsonNo .locale(executionContext.getExecutionConfig().getLocale()) .arguments(nodeType.toString(), this.schemaType.toString()).build()); } - - // TODO: Is this really necessary? - // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} - // Hack to catch patternProperties like "^foo":"value" -// if (this.schemaLocation.getName(-1).equals("type")) { -// if (rootNode.isArray()) { -// executionContext.getCollectorContext().getEvaluatedItems().add(instanceLocation); -// } else if (rootNode.isObject()) { -// executionContext.getCollectorContext().getEvaluatedProperties().add(instanceLocation); -// } -// } return Collections.emptySet(); } } From d00ef6c6b6f8c0cd90d7197aba9ec863a30c6e66 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:31:46 +0800 Subject: [PATCH 17/53] Update docs --- CHANGELOG.md | 5 +- README.md | 99 +++++++++++++++++++++++++++++++------ doc/compatibility.md | 10 +++- doc/quickstart.md | 114 ++++++++++++++++++------------------------- doc/upgrading.md | 61 +++++++++++++++++++++++ 5 files changed, 203 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a99331834..43b564cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Change Log All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/). +This format is based on [Keep a Changelog](http://keepachangelog.com/). + +This project does not adhere to [Semantic Versioning](https://semver.org/) and minor version changes can have incompatible API changes. These incompatible API changes will largely affect those who have custom validator or walker implementations. Those who just use the library to validate using the standard JSON Schema Draft specifications may not need changes. ## [Unreleased] diff --git a/README.md b/README.md index 621837844..d20dc29cf 100644 --- a/README.md +++ b/README.md @@ -14,33 +14,77 @@ This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. -In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. +In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. -## JSON Schema Draft Specification Compatibility +## JSON Schema Draft Specification compatibility Information on the compatibility support for each version, including known issues, can be found in the [Compatibility with JSON Schema versions](doc/compatibility.md) document. ## Upgrading to new versions -Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. This library can contain breaking changes in minor version releases. +This library can contain breaking changes in minor version releases. -For the latest version, please check the [Releases](https://github.com/networknt/json-schema-validator/releases) page. +Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. + +Information on the latest version can be found on the [Releases](https://github.com/networknt/json-schema-validator/releases) page. + +## Comparing against other implementations + +The [JSON Schema Validation Comparison +](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. +* [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional) +* [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance) + +The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report](https://bowtie.report/) that compares functional characteristics of different implementations, including non-Java implementations, but does not do any performance benchmarking. ## Why this library #### Performance -It is the fastest Java JSON Schema Validator as far as I know. Here is the testing result compare with the other two open-source implementations. It is about 32 times faster than the Fge and five times faster than the Everit. +This should be the fastest Java JSON Schema Validator implementation. + +The following is the benchmark results from [JSON Schema Validator Perftest](https://github.com/networknt/json-schema-validator-perftest) project that uses the [Java Microbenchmark Harness](https://github.com/openjdk/jmh). + +Note that the benchmark results are highly dependent on the input data workloads used for the validation. + +In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance) for benchmark results for more typical workloads -- fge: 7130ms -- everit-org: 1168ms -- networknt: 223ms +If performance is an important consideration, the specific sample workloads should be benchmarked, as there are different performance characteristics when certain keywords are used. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators, and annotation collection will adversely affect performance. -You can run the performance tests for three libraries from [https://github.com/networknt/json-schema-validator-perftest](https://github.com/networknt/json-schema-validator-perftest) +##### NetworkNT 1.3.1 + +``` +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.testValidate thrpt 10 6776.693 ± 115.309 ops/s +NetworkntBenchmark.testValidate:·gc.alloc.rate thrpt 10 971.191 ± 16.420 MB/sec +NetworkntBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 165318.816 ± 0.459 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 968.894 ± 51.234 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 164933.962 ± 8636.203 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.002 ± 0.001 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.274 ± 0.218 B/op +NetworkntBenchmark.testValidate:·gc.count thrpt 10 89.000 counts +NetworkntBenchmark.testValidate:·gc.time thrpt 10 99.000 ms +``` + +###### Everit 1.14.1 + +``` +Benchmark Mode Cnt Score Error Units +EveritBenchmark.testValidate thrpt 10 3719.192 ± 125.592 ops/s +EveritBenchmark.testValidate:·gc.alloc.rate thrpt 10 1448.208 ± 74.746 MB/sec +EveritBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 449621.927 ± 7400.825 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 1446.397 ± 79.919 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 449159.799 ± 18614.931 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.001 ± 0.001 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.364 ± 0.391 B/op +EveritBenchmark.testValidate:·gc.count thrpt 10 133.000 counts +EveritBenchmark.testValidate:·gc.time thrpt 10 148.000 ms +``` -#### Parser -It uses Jackson that is the most popular JSON parser in Java. If you are using Jackson parser already in your project, it is natural to choose this library over others for schema validation. +#### Jackson Parser + +This library uses [Jackson](https://github.com/FasterXML/jackson) which is a Java JSON parser that is widely used in other projects. If you are already using the Jackson parser in your project, it is natural to choose this library over others for schema validation. #### YAML Support @@ -50,9 +94,11 @@ The library works with JSON and YAML on both schema definitions and input data. The OpenAPI 3.0 specification is using JSON schema to validate the request/response, but there are some differences. With a configuration file, you can enable the library to work with OpenAPI 3.0 validation. -#### Dependency +#### Minimal Dependencies + +Following the design principle of the Light Platform, this library has minimal dependencies to ensure there are no dependency conflicts when using it. -Following the design principle of the Light Platform, this library has minimum dependencies to ensure there are no dependency conflicts when using it. +##### Required Dependencies The following are the dependencies that will automatically be included when this library is included. @@ -86,6 +132,8 @@ The following are the dependencies that will automatically be included when this ``` +##### Optional Dependencies + The following are the optional dependencies that may be required for certain options. These are not automatically included and setting the relevant option without adding the library will result in a `ClassNotFoundException`. @@ -101,6 +149,10 @@ These are not automatically included and setting the relevant option without add ``` +##### Excludable Dependencies + +The following are required dependencies that are automatically included, but can be explicitly excluded if they are not required. + The YAML dependency can be excluded if this is not required. Attempting to process schemas or input that are YAML will result in a `ClassNotFoundException`. ```xml @@ -136,7 +188,7 @@ This package is available on Maven central. com.networknt json-schema-validator - 1.2.0 + 1.3.1 ``` @@ -144,13 +196,13 @@ This package is available on Maven central. ```java dependencies { - implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.2.0'); + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.3.1'); } ``` ### Validating inputs against a schema -The following example demonstrates how inputs is validated against a schema. It comprises the following steps. +The following example demonstrates how inputs are validated against a schema. It comprises the following steps. * Creating a schema factory with the default schema dialect and how the schemas can be retrieved. * Configuring mapping the `$id` to a retrieval URI using `schemaMappers`. @@ -229,6 +281,21 @@ Set assertions = schema.validate(input, InputFormat.JSON, exe }); ``` +## Performance Considerations + +When the library creates a schema from the schema factory, it creates a distinct validator instance for each location on the evaluation path. This means if there are different `$ref` that reference the same schema location, different validator instances are created for each evaluation path. + +When the schema is created, the library will automatically preload all the validators needed and resolve references. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved. + +The `JsonSchema` created from the factory should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved. + +Collecting annotations will adversely affect validation performance. + +The earlier draft specifications contain less keywords that can potentially impact performance. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators. + +This does not mean that using a schema with a later draft specification will automatically cause a performance impact. For instance, the `properties` validator will perform checks to determine if annotations need to be collected, and checks if the meta schema contains the `unevaluatedProperties` keyword and whether the `unevaluatedProperties` keyword exists adjacent the evaluation path. + + ## [Quick Start](doc/quickstart.md) ## [Validators](doc/validators.md) diff --git a/doc/compatibility.md b/doc/compatibility.md index f20c97e2c..91c28f8e8 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -5,8 +5,14 @@ This implementation does not currently generate annotations. The `pattern` validator by default uses the JDK regular expression implementation which is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. The library can however be configured to use a ECMA-262 compliant regular expression implementation. ### Known Issues -* The `anyOf` applicator currently returns immediately on matching a schema. This results in the `unevaluatedItems` and `unevaluatedProperties` keywords potentially returning an incorrect result as the rest of the schemas in the `anyOf` aren't processed. -* The `unevaluatedItems` keyword does not currently consider `contains`. + +There are currently no known issues with the required functionality from the specification. + +The following are the tests results after running the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as at 29 Jan 2024 using version 1.3.1. As the test suite is continously updated, this can result in changes in the results subsequently. + +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| +| NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | ### Legend diff --git a/doc/quickstart.md b/doc/quickstart.md index 65467b817..73c7be412 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -1,83 +1,65 @@ ## Quick Start -To use the validator, we need to have both the `JsonSchema` object and `JsonNode` object constructed. -There are many ways to do that. -Here is base test class, that shows several ways to construct these from `String`, `Stream`, `Url`, and `JsonNode`. -Please pay attention to the `JsonSchemaFactory` class as it is the way to construct the `JsonSchema` object. +To use the validator, we need to have the `JsonSchema` loaded and cached. -```java -public class BaseJsonSchemaValidatorTest { +For simplicity the following test loads a schema from a `String` or `JsonNode`. Note that loading a schema in this manner is not recommended as a relative `$ref` will not be properly resolved as there is no base IRI. - private ObjectMapper mapper = new ObjectMapper(); +The preferred method of loading a schema is by using a `SchemaLocation` and by configuring the appropriate `SchemaMapper` and `SchemaLoader` on the `JsonSchemaFactory`. - protected JsonNode getJsonNodeFromClasspath(String name) throws IOException { - InputStream is1 = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(name); - return mapper.readTree(is1); - } +```java +package com.example; - protected JsonNode getJsonNodeFromStringContent(String content) throws IOException { - return mapper.readTree(content); - } +import static org.junit.jupiter.api.Assertions.assertEquals; - protected JsonNode getJsonNodeFromUrl(String url) throws IOException { - return mapper.readTree(new URL(url)); - } +import java.util.Set; - protected JsonSchema getJsonSchemaFromClasspath(String name) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - InputStream is = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(name); - return factory.getSchema(is); - } +import org.junit.jupiter.api.Test; - protected JsonSchema getJsonSchemaFromStringContent(String schemaContent) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(schemaContent); - } +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.*; +import com.networknt.schema.serialization.JsonMapperFactory; - protected JsonSchema getJsonSchemaFromUrl(String uri) throws URISyntaxException { +public class SampleTest { + @Test + void schemaFromString() throws JsonMappingException, JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(SchemaLocation.of(uri)); + /* + * This should be cached for performance. + * + * Loading from a String is not recommended as there is no base IRI to use for + * resolving relative $ref. + */ + JsonSchema schemaFromString = factory + .getSchema("{\"enum\":[1, 2, 3, 4],\"enumErrorCode\":\"Not in the list\"}"); + Set errors = schemaFromString.validate("7", InputFormat.JSON); + assertEquals(1, errors.size()); } - protected JsonSchema getJsonSchemaFromJsonNode(JsonNode jsonNode) { + @Test + void schemaFromJsonNode() throws JsonMappingException, JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(jsonNode); - } - - // Automatically detect version for given JsonNode - protected JsonSchema getJsonSchemaFromJsonNodeAutomaticVersion(JsonNode jsonNode) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(jsonNode)); - return factory.getSchema(jsonNode); - } - -} -``` -And the following is one of the test cases in one of the test classes that extend from the above base class. As you can see, it constructs `JsonSchema` and `JsonNode` from `String`. - -```java -class Sample extends BaseJsonSchemaValidatorTest { - - void test() { - JsonSchema schema = getJsonSchemaFromStringContent("{\"enum\":[1, 2, 3, 4],\"enumErrorCode\":\"Not in the list\"}"); - JsonNode node = getJsonNodeFromStringContent("7"); - Set errors = schema.validate(node); - assertThat(errors.size(), is(1)); - - // With automatic version detection - JsonNode schemaNode = getJsonNodeFromStringContent( - "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}"); - JsonSchema schema = getJsonSchemaFromJsonNodeAutomaticVersion(schemaNode); - - schema.initializeValidators(); // by default all schemas are loaded lazily. You can load them eagerly via - // initializeValidators() - - JsonNode node = getJsonNodeFromStringContent("{\"id\": \"2\"}"); - Set errors = schema.validate(node); - assertThat(errors.size(), is(1)); + JsonNode schemaNode = JsonMapperFactory.getInstance().readTree( + "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}"); + /* + * This should be cached for performance. + * + * Loading from a JsonNode is not recommended as there is no base IRI to use for + * resolving relative $ref. + * + * Note that the V4 from the factory is the default version if $schema is not + * specified. As $schema is specified in the data, V6 is used. + */ + JsonSchema schemaFromNode = factory.getSchema(schemaNode); + /* + * By default all schemas are preloaded eagerly but ref resolve failures are not + * thrown. You check if there are issues with ref resolving using + * initializeValidators() + */ + schemaFromNode.initializeValidators(); + Set errors = schemaFromNode.validate("{\"id\": \"2\"}", InputFormat.JSON); + assertEquals(1, errors.size()); } - } - ``` diff --git a/doc/upgrading.md b/doc/upgrading.md index 0eab6becf..0004a680f 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -2,6 +2,67 @@ This contains information on the notable or breaking changes in each version. +### 1.3.1 + +This does not contain any breaking changes from 1.3.0 + +This refactors the following keywords to improve performance and meet the functional requirements. + +In particular this converts the `unevaluatedItems` and `unevaluatedProperties` validators to use annotations to perform the evaluation instead of the current mechanism which affects performance. This also refactors `$recursiveRef` not to rely on that same mechanism. + +* `unevaluatedProperties` +* `unevaluatedItems` +* `properties` +* `patternProperties` +* `items` / `additionalItems` +* `prefixItems` / `items` +* `contains` +* `$recursiveRef` + +This also fixes the issue where the `unevaluatedItems` keyword does not take into account the `contains` keyword when performing the evaluation. + +This also fixes cases where `anyOf` short-circuits to not short-circuit the evaluation if a adjacent `unevaluatedProperties` or `unevaluatedItems` keyword exists. + +This should fix most of the remaining functional and performance issues. + +#### Functional + +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| +| NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | + +#### Performance + +##### NetworkNT 1.3.1 + +``` +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.testValidate thrpt 10 6776.693 ± 115.309 ops/s +NetworkntBenchmark.testValidate:·gc.alloc.rate thrpt 10 971.191 ± 16.420 MB/sec +NetworkntBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 165318.816 ± 0.459 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 968.894 ± 51.234 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 164933.962 ± 8636.203 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.002 ± 0.001 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.274 ± 0.218 B/op +NetworkntBenchmark.testValidate:·gc.count thrpt 10 89.000 counts +NetworkntBenchmark.testValidate:·gc.time thrpt 10 99.000 ms +``` + +###### Everit 1.14.1 + +``` +Benchmark Mode Cnt Score Error Units +EveritBenchmark.testValidate thrpt 10 3719.192 ± 125.592 ops/s +EveritBenchmark.testValidate:·gc.alloc.rate thrpt 10 1448.208 ± 74.746 MB/sec +EveritBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 449621.927 ± 7400.825 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 1446.397 ± 79.919 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 449159.799 ± 18614.931 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.001 ± 0.001 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.364 ± 0.391 B/op +EveritBenchmark.testValidate:·gc.count thrpt 10 133.000 counts +EveritBenchmark.testValidate:·gc.time thrpt 10 148.000 ms +``` + ### 1.3.0 This adds support for Draft 2020-12 From 8240445718ee0dd757c57b149e4c64930424a345 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 16:33:05 +0800 Subject: [PATCH 18/53] Add schemaNode and instanceNode to validation message and configure serialization --- .../schema/AdditionalPropertiesValidator.java | 4 +- .../com/networknt/schema/AnyOfValidator.java | 4 +- .../networknt/schema/BaseJsonValidator.java | 6 ++ .../com/networknt/schema/ConstValidator.java | 4 +- .../networknt/schema/ContainsValidator.java | 10 +-- .../schema/ContentEncodingValidator.java | 2 +- .../schema/ContentMediaTypeValidator.java | 2 +- .../schema/DependenciesValidator.java | 2 +- .../networknt/schema/DependentRequired.java | 2 +- .../com/networknt/schema/EnumValidator.java | 2 +- .../schema/ExclusiveMaximumValidator.java | 2 +- .../schema/ExclusiveMinimumValidator.java | 2 +- .../com/networknt/schema/FalseValidator.java | 2 +- .../com/networknt/schema/FormatValidator.java | 6 +- .../com/networknt/schema/ItemsValidator.java | 5 +- .../schema/ItemsValidator202012.java | 2 +- .../networknt/schema/MaxItemsValidator.java | 4 +- .../networknt/schema/MaxLengthValidator.java | 2 +- .../schema/MaxPropertiesValidator.java | 2 +- .../networknt/schema/MaximumValidator.java | 2 +- .../networknt/schema/MinItemsValidator.java | 4 +- .../networknt/schema/MinLengthValidator.java | 2 +- .../schema/MinMaxContainsValidator.java | 2 +- .../schema/MinPropertiesValidator.java | 2 +- .../networknt/schema/MinimumValidator.java | 2 +- .../networknt/schema/MultipleOfValidator.java | 2 +- .../networknt/schema/NotAllowedValidator.java | 2 +- .../com/networknt/schema/NotValidator.java | 4 +- .../com/networknt/schema/OneOfValidator.java | 2 +- .../schema/PatternPropertiesValidator.java | 11 ++- .../networknt/schema/PatternValidator.java | 2 +- .../schema/PrefixItemsValidator.java | 7 +- .../networknt/schema/PropertiesValidator.java | 2 +- .../schema/PropertyNamesValidator.java | 2 +- .../networknt/schema/ReadOnlyValidator.java | 2 +- .../schema/RecursiveRefValidator.java | 2 + .../networknt/schema/RequiredValidator.java | 2 +- .../com/networknt/schema/TypeValidator.java | 2 +- .../schema/UnevaluatedItemsValidator.java | 2 +- .../UnevaluatedPropertiesValidator.java | 4 +- .../networknt/schema/UnionTypeValidator.java | 2 +- .../schema/UniqueItemsValidator.java | 2 +- .../networknt/schema/ValidationMessage.java | 67 ++++++++++++++++++- .../networknt/schema/WriteOnlyValidator.java | 2 +- .../schema/format/DateTimeValidator.java | 2 +- .../schema/CustomMetaSchemaTest.java | 2 + .../schema/ValidationMessageTest.java | 36 ++++++++++ 47 files changed, 174 insertions(+), 66 deletions(-) create mode 100644 src/test/java/com/networknt/schema/ValidationMessageTest.java diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index c94ef8088..9f91b4eb5 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -104,7 +104,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().property(pname).instanceLocation(instanceLocation.append(pname)) + errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation.append(pname)) .locale(executionContext.getExecutionConfig().getLocale()).arguments(pname).build()); } else { if (additionalPropertiesSchema != null) { @@ -194,7 +194,7 @@ private boolean hasUnevaluatedPropertiesValidator() { public void preloadJsonSchema() { if(additionalPropertiesSchema != null) { additionalPropertiesSchema.initializeValidators(); - collectAnnotations(); } + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index a2693633a..a479b3702 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -105,7 +105,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(message().instanceLocation(instanceLocation) + allErrors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(DISCRIMINATOR_REMARK).build()); } else { @@ -132,7 +132,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { final Set errors = new LinkedHashSet<>(); - errors.add(message().instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments( "based on the provided discriminator. No alternative could be chosen based on the discriminator property") diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 29693e8b6..c78fbdadb 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -367,5 +367,11 @@ protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { } return hasValidator; } + + @Override + protected MessageSourceValidationMessage.Builder message() { + return super.message().schemaNode(this.schemaNode); + } + } diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 7c50dbab3..1cfb440db 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -36,12 +36,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()) .build()); } } else if (!schemaNode.equals(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 14bd7075f..351e111ad 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -90,12 +90,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (actual < m) { results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MIN_CONTAINS : ValidatorTypeCode.CONTAINS, - executionContext.getExecutionConfig().getLocale(), instanceLocation, m); + executionContext.getExecutionConfig().getLocale(), node, instanceLocation, m); } if (this.max != null && actual > this.max) { results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MAX_CONTAINS : ValidatorTypeCode.CONTAINS, - executionContext.getExecutionConfig().getLocale(), instanceLocation, this.max); + executionContext.getExecutionConfig().getLocale(), node, instanceLocation, this.max); } } @@ -147,11 +147,11 @@ public Set validate(ExecutionContext executionContext, JsonNo @Override public void preloadJsonSchema() { Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators); - collectAnnotations(); + collectAnnotations(); // cache the flag } private Set boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, - JsonNodePath instanceLocation, int bounds) { + JsonNode instanceNode, JsonNodePath instanceLocation, int bounds) { String messageKey = "contains"; if (ValidatorTypeCode.MIN_CONTAINS.equals(validatorTypeCode)) { messageKey = CONTAINS_MIN; @@ -159,7 +159,7 @@ private Set boundsViolated(ValidatorTypeCode validatorTypeCod messageKey = CONTAINS_MAX; } return Collections - .singleton(message().instanceLocation(instanceLocation).messageKey(messageKey) + .singleton(message().instanceNode(instanceNode).instanceLocation(instanceLocation).messageKey(messageKey) .locale(locale).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()) .code(validatorTypeCode.getErrorCode()).type(validatorTypeCode.getValue()).build()); } diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java index 579e67921..60f4e01e9 100644 --- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -66,7 +66,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).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..85b327921 100644 --- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -90,7 +90,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).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..c08c504e1 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -61,7 +61,7 @@ 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()) .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..0d5ca71f5 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -54,7 +54,7 @@ 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) + errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(field, pname) .build()); } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index eb006b584..67531ece5 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -87,7 +87,7 @@ 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) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build()); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index b67894600..291fe2f8a 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -103,7 +103,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) .build()); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index db0655330..9c78304aa 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -110,7 +110,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) .build()); } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 675b4a601..75599d318 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -32,7 +32,7 @@ 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) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).build()); } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index cb3cb1f58..b2a419850 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -52,14 +52,14 @@ 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()) .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 +68,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/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 4436794f5..249f6c16b 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -172,7 +172,7 @@ private boolean doValidate(ExecutionContext executionContext, 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()).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()).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..4486b1e18 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -45,7 +45,7 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(maxLength).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index 56eadd1f1..face74400 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -41,7 +41,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index 3bc086073..7e5f2b76c 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -113,7 +113,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) .build()); } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 969b399cb..b81c7cc03 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -40,12 +40,12 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, node.size()).build()); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, 1).build()); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 7038a2bd1..f609cab52 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -46,7 +46,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(minLength).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index 419ca6f1c..10865c6bd 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -62,7 +62,7 @@ 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()) .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..1d7366542 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -41,7 +41,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(min).build()); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 27ff40253..898b48337 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -120,7 +120,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) .build()); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 7d1d73f45..3a7bf3768 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -46,7 +46,7 @@ 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) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(divisor).build()); } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index c6c4e80fe..5931a70a7 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -49,7 +49,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().property(fieldName).instanceLocation(instanceLocation.append(fieldName)) + errors.add(message().property(fieldName).instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index c6f8f1203..c44ffc9a2 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -41,7 +41,7 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); if (errors.isEmpty()) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) .build()); } @@ -58,7 +58,7 @@ 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) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) .build()); } diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index bb59e0f1d..42da9879d 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -88,7 +88,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. if (numberOfValidSchema != 1) { - ValidationMessage message = message().instanceLocation(instanceLocation) + ValidationMessage message = message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(Integer.toString(numberOfValidSchema)).build(); if (this.failFast) { diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index 35a01c11c..17921ee0b 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -53,7 +53,7 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } Set errors = null; - Set matchedInstancePropertyNames = new LinkedHashSet<>(); + Set matchedInstancePropertyNames = null; Iterator names = node.fieldNames(); while (names.hasNext()) { String name = names.next(); @@ -63,7 +63,12 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNodePath path = instanceLocation.append(name); Set results = entry.getValue().validate(executionContext, n, rootNode, path); if (results.isEmpty()) { - matchedInstancePropertyNames.add(name); + if (collectAnnotations()) { + if (matchedInstancePropertyNames == null) { + matchedInstancePropertyNames = new LinkedHashSet<>(); + } + matchedInstancePropertyNames.add(name); + } } else { if (errors == null) { errors = new LinkedHashSet<>(); @@ -96,6 +101,6 @@ private boolean hasUnevaluatedPropertiesValidator() { @Override public void preloadJsonSchema() { preloadJsonSchemas(schemas.values()); - collectAnnotations(); + 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..218226deb 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -58,7 +58,7 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.pattern).build()); } } catch (JsonSchemaException e) { diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index c062b1a06..922aafc09 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -60,14 +60,11 @@ 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()) { -// evaluatedItems.add(path); - } else { + if (!results.isEmpty()) { errors.addAll(results); } } @@ -174,7 +171,7 @@ private boolean hasUnevaluatedItemsValidator() { @Override public void preloadJsonSchema() { preloadJsonSchemas(this.tupleSchema); - collectAnnotations(); + 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 9fb0ac7b8..bac7522f3 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -214,6 +214,6 @@ public Map getSchemas() { @Override public void preloadJsonSchema() { preloadJsonSchemas(this.schemas.values()); - collectAnnotations(); + 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..7d5bad905 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -48,7 +48,7 @@ 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()) + errors.add(message().property(pname).instanceNode(node).instanceLocation(schemaError.getInstanceLocation()) .locale(executionContext.getExecutionConfig().getLocale()).arguments(msg).build()); } } diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index a3d1e4f22..96c2a9fad 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -40,7 +40,7 @@ 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) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 744136e3a..a913e7bef 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -35,6 +35,8 @@ public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluat ValidationMessage validationMessage = ValidationMessage.builder() .type(ValidatorTypeCode.RECURSIVE_REF.getValue()).code("internal.invalidRecursiveRef") .message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").instanceLocation(schemaLocation.getFragment()) + .instanceNode(this.schemaNode) + .schemaNode(this.schemaNode) .evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 369e913f1..c7b10687a 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -57,7 +57,7 @@ public Set validate(ExecutionContext executionContext, JsonNo *

* @see Basic */ - errors.add(message().property(fieldName).instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).property(fieldName).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); } } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index a4b40a1c6..0da420ccc 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -57,7 +57,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(nodeType.toString(), this.schemaType.toString()).build()); } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 51460b75e..be662082e 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -198,7 +198,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } else { // Report these as unevaluated paths or not matching the unevalutedItems schema messages = messages.stream() - .map(m -> message().instanceLocation(m.getInstanceLocation()) + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) .locale(executionContext.getExecutionConfig().getLocale()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 053ab585d..1dec34593 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -121,7 +121,7 @@ public Set validate(ExecutionContext executionContext, JsonNo evaluatedProperties.add(fieldName); if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { // All fails as "unevaluatedProperties: false" - messages.add(message().instanceLocation(instanceLocation.append(fieldName)) + messages.add(message().instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) .locale(executionContext.getExecutionConfig().getLocale()).build()); } else { messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, @@ -133,7 +133,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // Report these as unevaluated paths or not matching the unevaluatedProperties // schema messages = messages.stream() - .map(m -> message().instanceLocation(m.getInstanceLocation()) + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) .locale(executionContext.getExecutionConfig().getLocale()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index e23b316ca..dbfae34c1 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -78,7 +78,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!valid) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(nodeType.toString(), error) .build()); } diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 1c7655246..4223e0cc2 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -43,7 +43,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).build()); } } diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 72d1df861..0bb24160d 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -16,6 +16,13 @@ package com.networknt.schema; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.networknt.schema.i18n.MessageFormatter; import com.networknt.schema.utils.CachingSupplier; import com.networknt.schema.utils.StringUtils; @@ -32,21 +39,30 @@ * "https://github.com/json-schema-org/json-schema-spec/blob/main/jsonschema-validation-output-machines.md">JSON * Schema */ +@JsonIgnoreProperties({ "messageSupplier", "schemaNode", "instanceNode", "valid" }) +@JsonPropertyOrder({ "type", "code", "message", "instanceLocation", "property", "evaluationPath", "schemaLocation", + "messageKey", "arguments", "details" }) +@JsonInclude(Include.NON_NULL) public class ValidationMessage { private final String type; private final String code; + @JsonSerialize(using = ToStringSerializer.class) private final JsonNodePath evaluationPath; + @JsonSerialize(using = ToStringSerializer.class) private final SchemaLocation schemaLocation; + @JsonSerialize(using = ToStringSerializer.class) private final JsonNodePath instanceLocation; private final String property; private final Object[] arguments; - private final Map details; private final String messageKey; private final Supplier messageSupplier; + private final Map details; + private final JsonNode instanceNode; + private final JsonNode schemaNode; ValidationMessage(String type, String code, JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNodePath instanceLocation, String property, Object[] arguments, Map details, - String messageKey, Supplier messageSupplier) { + String messageKey, Supplier messageSupplier, JsonNode instanceNode, JsonNode schemaNode) { super(); this.type = type; this.code = code; @@ -58,6 +74,8 @@ public class ValidationMessage { this.details = details; this.messageKey = messageKey; this.messageSupplier = messageSupplier; + this.instanceNode = instanceNode; + this.schemaNode = schemaNode; } public String getCode() { @@ -97,6 +115,37 @@ public SchemaLocation getSchemaLocation() { return schemaLocation; } + /** + * Returns the instance node which was evaluated. + *

+ * This corresponds with the instance location. + * + * @return the instance node + */ + public JsonNode getInstanceNode() { + return instanceNode; + } + + /** + * Returns the schema node which was evaluated. + *

+ * This corresponds with the schema location. + * + * @return the schema node + */ + public JsonNode getSchemaNode() { + return schemaNode; + } + + /** + * Returns the property with the error. + *

+ * For instance, for the required validator the instance location does not + * contain the missing property name as the instance must refer to the input + * data. + * + * @return the property name + */ public String getProperty() { return property; } @@ -186,6 +235,8 @@ public static abstract class BuilderSupport { protected Supplier messageSupplier; protected MessageFormatter messageFormatter; protected String messageKey; + protected JsonNode instanceNode; + protected JsonNode schemaNode; public S type(String type) { this.type = type; @@ -288,6 +339,16 @@ public S messageKey(String messageKey) { this.messageKey = messageKey; return self(); } + + public S instanceNode(JsonNode instanceNode) { + this.instanceNode = instanceNode; + return self(); + } + + public S schemaNode(JsonNode schemaNode) { + this.schemaNode = schemaNode; + return self(); + } public ValidationMessage build() { Supplier messageSupplier = this.messageSupplier; @@ -308,7 +369,7 @@ public ValidationMessage build() { messageSupplier = new CachingSupplier<>(() -> formatter.format(objs)); } return new ValidationMessage(type, code, evaluationPath, schemaLocation, instanceLocation, - property, arguments, details, messageKey, messageSupplier); + property, arguments, details, messageKey, messageSupplier, this.instanceNode, this.schemaNode); } protected Object[] getMessageArguments() { diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index 71171920c..a790b839d 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -24,7 +24,7 @@ public WriteOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluation public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); if (this.writeOnly) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 5e062b2d8..d7aebb774 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -55,7 +55,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!isLegalDateTime(node.textValue())) { if (assertionsEnabled) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) .build()); } diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index 7e427c2ef..936d4ab4b 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -69,6 +69,8 @@ public Set validate(ExecutionContext executionContext, JsonNo String valueName = enumNames.get(idx); Set messages = new HashSet<>(); ValidationMessage validationMessage = ValidationMessage.builder().type(keyword) + .schemaNode(node) + .instanceNode(node) .code("tests.example.enumNames").message("{0}: enumName is {1}").instanceLocation(instanceLocation) .arguments(valueName).build(); messages.add(validationMessage); diff --git a/src/test/java/com/networknt/schema/ValidationMessageTest.java b/src/test/java/com/networknt/schema/ValidationMessageTest.java new file mode 100644 index 000000000..1b3709d55 --- /dev/null +++ b/src/test/java/com/networknt/schema/ValidationMessageTest.java @@ -0,0 +1,36 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * ValidationMessageTest. + */ +public class ValidationMessageTest { + @Test + void testSerialization() throws JsonProcessingException { + String value = JsonMapperFactory.getInstance() + .writeValueAsString(ValidationMessage.builder().messageSupplier(() -> "hello") + .schemaLocation(SchemaLocation.of("https://www.example.com/#defs/definition")).build()); + assertEquals("{\"message\":\"hello\",\"schemaLocation\":\"https://www.example.com/#defs/definition\"}", value); + } +} From 7584e17475f9e5cda1368c444d0bb3978e744238 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 17:07:34 +0800 Subject: [PATCH 19/53] Fail fast based on execution context config --- .../schema/AdditionalPropertiesValidator.java | 6 ++- .../com/networknt/schema/AnyOfValidator.java | 1 + .../networknt/schema/BaseJsonValidator.java | 7 ++- .../com/networknt/schema/ConstValidator.java | 3 +- .../networknt/schema/ContainsValidator.java | 10 +++-- .../schema/ContentEncodingValidator.java | 4 +- .../schema/ContentMediaTypeValidator.java | 4 +- .../schema/DependenciesValidator.java | 1 + .../networknt/schema/DependentRequired.java | 3 +- .../com/networknt/schema/EnumValidator.java | 3 +- .../schema/ExclusiveMaximumValidator.java | 5 ++- .../schema/ExclusiveMinimumValidator.java | 5 ++- .../com/networknt/schema/ExecutionConfig.java | 44 +++++++++++++++++++ .../com/networknt/schema/FalseValidator.java | 3 +- .../com/networknt/schema/FormatValidator.java | 1 + .../com/networknt/schema/ItemsValidator.java | 3 +- .../java/com/networknt/schema/JsonSchema.java | 1 + .../networknt/schema/MaxItemsValidator.java | 8 +++- .../networknt/schema/MaxLengthValidator.java | 3 +- .../schema/MaxPropertiesValidator.java | 3 +- .../networknt/schema/MaximumValidator.java | 5 ++- .../MessageSourceValidationMessage.java | 18 +++++--- .../networknt/schema/MinItemsValidator.java | 7 ++- .../networknt/schema/MinLengthValidator.java | 3 +- .../schema/MinMaxContainsValidator.java | 4 +- .../schema/MinPropertiesValidator.java | 3 +- .../networknt/schema/MinimumValidator.java | 5 ++- .../networknt/schema/MultipleOfValidator.java | 3 +- .../networknt/schema/NotAllowedValidator.java | 6 ++- .../com/networknt/schema/NotValidator.java | 6 ++- .../com/networknt/schema/OneOfValidator.java | 3 +- .../networknt/schema/PatternValidator.java | 3 +- .../schema/PropertyNamesValidator.java | 6 ++- .../networknt/schema/ReadOnlyValidator.java | 3 +- .../networknt/schema/RequiredValidator.java | 3 +- .../com/networknt/schema/TypeValidator.java | 1 + .../schema/UnevaluatedItemsValidator.java | 3 +- .../UnevaluatedPropertiesValidator.java | 6 ++- .../networknt/schema/UnionTypeValidator.java | 3 +- .../schema/UniqueItemsValidator.java | 3 +- .../schema/ValidationMessageHandler.java | 17 +++---- .../networknt/schema/WriteOnlyValidator.java | 3 +- .../schema/format/DateTimeValidator.java | 5 ++- 43 files changed, 168 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 9f91b4eb5..768549bb6 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -104,8 +104,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation.append(pname)) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(pname).build()); + errors.add(message().instanceNode(node).property(pname) + .instanceLocation(instanceLocation.append(pname)) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(pname).build()); } else { if (additionalPropertiesSchema != null) { ValidatorState state = executionContext.getValidatorState(); diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index a479b3702..179e1c480 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -107,6 +107,7 @@ public Set validate(ExecutionContext executionContext, JsonNo allErrors.addAll(errors); allErrors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(DISCRIMINATOR_REMARK).build()); } else { // Clear all errors. diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index c78fbdadb..0890274fb 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -45,16 +45,15 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(validationContext != null - && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), - errorMessageType, + super(errorMessageType, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().isCustomMessageSupported() : true, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getMessageSource() : DefaultMessageSource.getInstance(), - keyword, parentSchema, schemaLocation, evaluationPath); + keyword, + parentSchema, schemaLocation, evaluationPath); this.validationContext = validationContext; this.schemaNode = schemaNode; this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 1cfb440db..ceb34c8ff 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -37,7 +37,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(schemaNode.asText()) .build()); } } else if (!schemaNode.equals(node)) { diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 351e111ad..aa54ff9dd 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -90,12 +90,14 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (actual < m) { results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MIN_CONTAINS : ValidatorTypeCode.CONTAINS, - executionContext.getExecutionConfig().getLocale(), node, instanceLocation, m); + executionContext.getExecutionConfig().getLocale(), + executionContext.getExecutionConfig().isFailFast(), node, instanceLocation, m); } if (this.max != null && actual > this.max) { results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MAX_CONTAINS : ValidatorTypeCode.CONTAINS, - executionContext.getExecutionConfig().getLocale(), node, instanceLocation, this.max); + executionContext.getExecutionConfig().getLocale(), + executionContext.getExecutionConfig().isFailFast(), node, instanceLocation, this.max); } } @@ -150,7 +152,7 @@ public void preloadJsonSchema() { collectAnnotations(); // cache the flag } - private Set boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, + private Set boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, boolean failFast, JsonNode instanceNode, JsonNodePath instanceLocation, int bounds) { String messageKey = "contains"; if (ValidatorTypeCode.MIN_CONTAINS.equals(validatorTypeCode)) { @@ -160,7 +162,7 @@ private Set boundsViolated(ValidatorTypeCode validatorTypeCod } return Collections .singleton(message().instanceNode(instanceNode).instanceLocation(instanceLocation).messageKey(messageKey) - .locale(locale).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()) + .locale(locale).failFast(failFast).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()) .code(validatorTypeCode.getErrorCode()).type(validatorTypeCode.getValue()).build()); } diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java index 60f4e01e9..3318bda7e 100644 --- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -67,7 +67,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!matches(node.asText())) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentEncoding).build()); + .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 85b327921..a7e2cc405 100644 --- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -91,7 +91,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!matches(node.asText())) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentMediaType).build()); + .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 c08c504e1..e5fc60629 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -63,6 +63,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.get(field) == null) { 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 0d5ca71f5..62c9f4fc1 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -55,7 +55,8 @@ public Set validate(ExecutionContext executionContext, JsonNo for (String field : dependencies) { if (node.get(field) == null) { errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(field, pname) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(field, pname) .build()); } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 67531ece5..492d79b42 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -88,7 +88,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build()); + .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 291fe2f8a..d3ab8bb8a 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -104,8 +104,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (typedMaximum.crossesThreshold(node)) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) - .build()); + .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 9c78304aa..908a2f02d 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -111,8 +111,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (typedMinimum.crossesThreshold(node)) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) - .build()); + .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..95158f262 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -25,6 +25,9 @@ */ public class ExecutionConfig { private Locale locale = Locale.ROOT; + + private boolean annotationsCollectionEnabled = false; + private Predicate annotationAllowedPredicate = (keyword) -> true; /** @@ -32,6 +35,11 @@ public class ExecutionConfig { */ private Boolean formatAssertionsEnabled = null; + /** + * Determine if the validation execution can fail fast. + */ + private boolean failFast = false; + public Locale getLocale() { return locale; } @@ -113,4 +121,40 @@ 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 annotations collection is enabled. + * + * @return if annotations collection is enabled + */ + protected boolean isAnnotationsCollectionEnabled() { + return annotationsCollectionEnabled; + } + + /** + * Sets whether to annotation collection is enabled. + * + * @param annotationsCollectionEnabled true to enable annotation collection + */ + protected void setAnnotationsCollectionEnabled(boolean annotationsCollectionEnabled) { + this.annotationsCollectionEnabled = annotationsCollectionEnabled; + } } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 75599d318..b6b77860b 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -33,6 +33,7 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); // For the false validator, it is always not valid return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } } diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index b2a419850..eaa057897 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -54,6 +54,7 @@ public Set validate(ExecutionContext executionContext, JsonNo // leading and trailing spaces 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("%")) { diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 249f6c16b..1282ce0a0 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -173,7 +173,8 @@ private boolean doValidate(ExecutionContext executionContext, Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(message().instanceNode(node).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().instanceNode(node).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 4486b1e18..ad95d50fd 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -46,7 +46,8 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(maxLength).build()); + .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 face74400..9b8c21693 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -42,7 +42,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); + .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 7e5f2b76c..85fe78bb2 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -114,8 +114,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (typedMaximum.crossesThreshold(node)) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) - .build()); + .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 b81c7cc03..5c4aa4bc4 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -41,12 +41,15 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, node.size()).build()); + .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().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, 1).build()); + .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 f609cab52..a0013855c 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -47,7 +47,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(minLength).build()); + .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 10865c6bd..ef2f91fd6 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -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().instanceNode(node).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 1d7366542..da6f722a3 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -42,7 +42,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min).build()); + .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 898b48337..589772fc2 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -121,8 +121,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (typedMinimum.crossesThreshold(node)) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) - .build()); + .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 3a7bf3768..0e94fcf18 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -47,7 +47,8 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(divisor).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(divisor).build()); } } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index 5931a70a7..e3988b799 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -49,8 +49,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().property(fieldName).instanceNode(node).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 c44ffc9a2..310122bf0 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -42,7 +42,8 @@ public Set validate(ExecutionContext executionContext, JsonNo errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); if (errors.isEmpty()) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString()) .build()); } return Collections.emptySet(); @@ -59,7 +60,8 @@ public Set walk(ExecutionContext executionContext, JsonNode n Set errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); if (errors.isEmpty()) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) + .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 42da9879d..1104b8a79 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -90,8 +90,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (numberOfValidSchema != 1) { ValidationMessage message = message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(Integer.toString(numberOfValidSchema)).build(); - if (this.failFast) { + if (executionContext.getExecutionConfig().isFailFast()) { throw new JsonSchemaException(message); } errors.add(message); diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index 218226deb..9b7f087a8 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -59,7 +59,8 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.pattern).build()); + .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/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index 7d5bad905..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).instanceNode(node).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 96c2a9fad..462e228d4 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -41,7 +41,8 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); if (this.readOnly) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index c7b10687a..74390fc00 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -58,7 +58,8 @@ public Set validate(ExecutionContext executionContext, JsonNo * @see Basic */ errors.add(message().instanceNode(node).property(fieldName).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(fieldName).build()); } } diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 0da420ccc..5a2bf9fd0 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -59,6 +59,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(nodeType.toString(), this.schemaType.toString()).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index be662082e..e190410e9 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -199,7 +199,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // Report these as unevaluated paths or not matching the unevalutedItems schema messages = messages.stream() .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) - .locale(executionContext.getExecutionConfig().getLocale()).build()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 1dec34593..5b408e529 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -122,7 +122,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { // All fails as "unevaluatedProperties: false" messages.add(message().instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } else { messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, instanceLocation.append(fieldName))); @@ -134,7 +135,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // schema messages = messages.stream() .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) - .locale(executionContext.getExecutionConfig().getLocale()).build()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)); } executionContext.getAnnotations() diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index dbfae34c1..ac5294c90 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -79,7 +79,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!valid) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(nodeType.toString(), error) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(nodeType.toString(), error) .build()); } diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 4223e0cc2..0ba3aa8c6 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -44,7 +44,8 @@ public Set validate(ExecutionContext executionContext, JsonNo for (JsonNode n : node) { if (!set.add(n)) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 61c3260b6..81fe3030c 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -10,7 +10,6 @@ import java.util.Objects; public abstract class ValidationMessageHandler { - protected boolean failFast; protected final MessageSource messageSource; protected ErrorMessageType errorMessageType; @@ -25,10 +24,9 @@ public abstract class ValidationMessageHandler { protected Keyword keyword; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, - boolean customErrorMessagesEnabled, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, - SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - this.failFast = failFast; + protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled, + MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, + JsonNodePath evaluationPath) { this.errorMessageType = errorMessageType; this.messageSource = messageSource; this.schemaLocation = Objects.requireNonNull(schemaLocation); @@ -44,7 +42,6 @@ protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessa * @param copy to copy from */ protected ValidationMessageHandler(ValidationMessageHandler copy) { - this.failFast = copy.failFast; this.messageSource = copy.messageSource; this.errorMessageType = copy.errorMessageType; this.schemaLocation = copy.schemaLocation; @@ -57,8 +54,8 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { } protected MessageSourceValidationMessage.Builder message() { - return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, message -> { - if (canFastFail()) { + return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, (message, failFast) -> { + if (failFast && canFailFast()) { throw new JsonSchemaException(message); } }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) @@ -75,8 +72,8 @@ protected ErrorMessageType getErrorMessageType() { * * @return true if it can fast fail */ - protected boolean canFastFail() { - return this.failFast && !hasApplicatorInEvaluationPath(); + protected boolean canFailFast() { + return !hasApplicatorInEvaluationPath(); } /** diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index a790b839d..b05a24428 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -25,7 +25,8 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); if (this.writeOnly) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index d7aebb774..1ba046e9a 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -56,8 +56,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!isLegalDateTime(node.textValue())) { if (assertionsEnabled) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) - .build()); + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(node.textValue(), DATETIME).build()); } } return Collections.emptySet(); From baef348aaadf252498a9e90578b24735c1df75d6 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:11:39 +0800 Subject: [PATCH 20/53] Fix fail fast --- .../schema/FailFastAssertionException.java | 30 +++++++++++++++++++ .../networknt/schema/JsonSchemaException.java | 10 ++++++- .../com/networknt/schema/OneOfValidator.java | 3 -- .../schema/ValidationMessageHandler.java | 23 ++++++++++---- 4 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/networknt/schema/FailFastAssertionException.java 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..45dd3804e --- /dev/null +++ b/src/main/java/com/networknt/schema/FailFastAssertionException.java @@ -0,0 +1,30 @@ +/* + * 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 assertion happens and the evaluation can fail fast. + */ +public class FailFastAssertionException extends JsonSchemaException { + private static final long serialVersionUID = 1L; + + public FailFastAssertionException(ValidationMessage validationMessage) { + super(Objects.requireNonNull(validationMessage)); + } +} 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/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 1104b8a79..12ebb0766 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -92,9 +92,6 @@ public Set validate(ExecutionContext executionContext, JsonNo .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(Integer.toString(numberOfValidSchema)).build(); - if (executionContext.getExecutionConfig().isFailFast()) { - throw new JsonSchemaException(message); - } errors.add(message); errors.addAll(childErrors); } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 81fe3030c..cde6904ff 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -56,7 +56,7 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { protected MessageSourceValidationMessage.Builder message() { return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, (message, failFast) -> { if (failFast && canFailFast()) { - throw new JsonSchemaException(message); + throw new FailFastAssertionException(message); } }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) .evaluationPath(this.evaluationPath).type(this.keyword != null ? this.keyword.getValue() : null) @@ -96,7 +96,7 @@ private boolean hasApplicatorInEvaluationPath() { * @return true if anyOf is in the evaluation path */ private boolean hasAnyOfInEvaluationPath() { - return this.evaluationPath.contains(ValidatorTypeCode.ANY_OF.getValue()); + return hasKeywordInEvaluationPath(ValidatorTypeCode.ANY_OF.getValue()); } /** @@ -105,7 +105,7 @@ private boolean hasAnyOfInEvaluationPath() { * @return true if if is in the evaluation path */ private boolean hasIfInEvaluationPath() { - return this.evaluationPath.contains(ValidatorTypeCode.IF_THEN_ELSE.getValue()); + return hasKeywordInEvaluationPath(ValidatorTypeCode.IF_THEN_ELSE.getValue()); } /** @@ -114,7 +114,7 @@ private boolean hasIfInEvaluationPath() { * @return true if not is in the evaluation path */ private boolean hasNotInEvaluationPath() { - return this.evaluationPath.contains(ValidatorTypeCode.NOT.getValue()); + return hasKeywordInEvaluationPath(ValidatorTypeCode.NOT.getValue()); } /** @@ -123,7 +123,20 @@ private boolean hasNotInEvaluationPath() { * @return true if oneOf is in the evaluation path */ protected boolean hasOneOfInEvaluationPath() { - return this.evaluationPath.contains(ValidatorTypeCode.ONE_OF.getValue()); + return hasKeywordInEvaluationPath(ValidatorTypeCode.ONE_OF.getValue()); + } + + /** + * Determines if keyword is in the evaluation path. + * + * @param keyword the keyword + * @return true if the keyword is in the evaluation path + */ + private boolean hasKeywordInEvaluationPath(String keyword) { + // Parent is used as if the current path is an applicator this should still + // throw if there is no other applicators in the rest of the evaluation path + JsonNodePath parent = this.evaluationPath.getParent(); + return parent != null ? parent.contains(keyword) : false; } protected void parseErrorCode(String errorCodeKey) { From 975ab4eb7a9be17bb80481864201ff29b11191ad Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:30:56 +0800 Subject: [PATCH 21/53] Refactor annotation config. --- .../com/networknt/schema/ExecutionConfig.java | 141 ++++++++++-------- .../com/networknt/schema/OutputFormat.java | 6 +- 2 files changed, 85 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index 95158f262..6faf7ea45 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -24,11 +24,26 @@ * Configuration per execution. */ public class ExecutionConfig { + /** + * The locale to use for formatting messages. + */ private Locale locale = Locale.ROOT; - - private boolean annotationsCollectionEnabled = false; - - 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) -> true; /** * Since Draft 2019-09 format assertions are not enabled by default. @@ -40,62 +55,22 @@ public class ExecutionConfig { */ private boolean failFast = false; - public Locale getLocale() { - return locale; - } - - public void setLocale(Locale locale) { - this.locale = Objects.requireNonNull(locale, "Locale must not be null"); - } - /** - * Gets the predicate to determine if annotation collection is allowed for a - * particular keyword. - *

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

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

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

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

- * unevaluatedItems depends on items/prefixItems, additionalItems/items and - * contains. + * 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"); } /** @@ -141,20 +116,68 @@ public void setFailFast(boolean failFast) { } /** - * Return if annotations collection is enabled. + * 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 annotations collection is enabled + * @return if annotation collection is enabled */ - protected boolean isAnnotationsCollectionEnabled() { - return annotationsCollectionEnabled; + 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 annotationsCollectionEnabled true to enable annotation collection + * @param annotationCollectionEnabled true to enable annotation collection */ - protected void setAnnotationsCollectionEnabled(boolean annotationsCollectionEnabled) { - this.annotationsCollectionEnabled = annotationsCollectionEnabled; + 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 collect all 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 collect all 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/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java index 6f8e84d05..d798c344d 100644 --- a/src/main/java/com/networknt/schema/OutputFormat.java +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -66,7 +66,7 @@ T format(Set validationMessages, ExecutionContext executionCo public static class Default implements OutputFormat> { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + executionContext.getExecutionConfig().setAnnotationCollectionPredicate( Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); } @@ -83,7 +83,7 @@ public Set format(Set validationMessages, public static class Flag implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + executionContext.getExecutionConfig().setAnnotationCollectionPredicate( Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); } @@ -100,7 +100,7 @@ public FlagOutput format(Set validationMessages, ExecutionCon public static class Boolean implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( + executionContext.getExecutionConfig().setAnnotationCollectionPredicate( Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); } From 210117b0648512fc9c12328f3d44b52470503dbd Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 18:36:05 +0800 Subject: [PATCH 22/53] Update doc --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d20dc29cf..f778ee210 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-0 In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. -## JSON Schema Draft Specification compatibility +## JSON Schema Specification compatibility Information on the compatibility support for each version, including known issues, can be found in the [Compatibility with JSON Schema versions](doc/compatibility.md) document. From 8d59d988eebbefdfebcffa2f2e7a8f9084a062f7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:42:29 +0800 Subject: [PATCH 23/53] Fix 939 and add 940 test --- .../java/com/networknt/schema/JsonSchema.java | 27 ++++++--- .../com/networknt/schema/Version202012.java | 1 + .../com/networknt/schema/Issue939Test.java | 57 +++++++++++++++++++ .../com/networknt/schema/Issue940Test.java | 37 ++++++++++++ 4 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/test/java/com/networknt/schema/Issue939Test.java create mode 100644 src/test/java/com/networknt/schema/Issue940Test.java diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 886deb0b5..1e9f23be8 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -247,6 +247,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 @@ -257,10 +261,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); @@ -273,9 +275,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. @@ -300,7 +310,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()); diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 581278369..6405fc627 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -45,6 +45,7 @@ public JsonMetaSchema getInstance() { new NonValidationKeyword("$defs"), new NonValidationKeyword("$anchor"), new NonValidationKeyword("$dynamicAnchor"), + new NonValidationKeyword("$vocabulary"), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), diff --git a/src/test/java/com/networknt/schema/Issue939Test.java b/src/test/java/com/networknt/schema/Issue939Test.java new file mode 100644 index 000000000..19b36c73d --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue939Test.java @@ -0,0 +1,57 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue939Test { + @Test + void shouldNotThrowException() { + String schema = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"additionalProperties\": false,\r\n" + + " \"required\": [\r\n" + + " \"someUuid\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"someUuid\": {\r\n" + + " \"$ref\": \"#/definitions/uuid\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"definitions\": {\r\n" + + " \"uuid\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"pattern\": \"^[0-9a-f]{8}(\\\\\\\\-[0-9a-f]{4}){3}\\\\\\\\-[0-9a-f]{12}$\",\r\n" + + " \"minLength\": 36,\r\n" + + " \"maxLength\": 36\r\n" + + " }\r\n" + + " }\r\n" + + " }"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schema); + assertDoesNotThrow(() -> jsonSchema.initializeValidators()); + Set assertions = jsonSchema + .validate("{\"someUuid\":\"invalid\"}", InputFormat.JSON); + assertEquals(2, assertions.size()); + } +} diff --git a/src/test/java/com/networknt/schema/Issue940Test.java b/src/test/java/com/networknt/schema/Issue940Test.java new file mode 100644 index 000000000..de30e9bff --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue940Test.java @@ -0,0 +1,37 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue940Test { + @Test + void shouldNotThrowException() { + String schema = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$ref\": \"#/$defs/greeting\",\r\n" + + " \"$defs\": {\r\n" + + " \"greeting\": {}\r\n" + + " }\r\n" + + "}"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schema); + assertDoesNotThrow(() -> jsonSchema.initializeValidators()); + } +} From f33a384558b4f401e7714744be229387e6ac6de9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 29 Jan 2024 23:09:55 +0800 Subject: [PATCH 24/53] Fix 936 --- .../schema/InvalidSchemaException.java | 35 ++++++++ .../java/com/networknt/schema/JsonSchema.java | 30 ++++++- .../networknt/schema/JsonSchemaFactory.java | 21 ++--- .../schema/JsonSchemaIdValidator.java | 89 +++++++++++++++++++ .../schema/SchemaValidatorsConfig.java | 21 +++++ .../DefaultJsonSchemaIdValidatorTest.java | 70 +++++++++++++++ .../com/networknt/schema/Issue347Test.java | 6 +- .../com/networknt/schema/Issue936Test.java | 40 +++++++++ src/test/resources/schema/id-relative.json | 4 + src/test/resources/schema/issue936.json | 4 + 10 files changed, 305 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/networknt/schema/InvalidSchemaException.java create mode 100644 src/main/java/com/networknt/schema/JsonSchemaIdValidator.java create mode 100644 src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java create mode 100644 src/test/java/com/networknt/schema/Issue936Test.java create mode 100644 src/test/resources/schema/id-relative.json create mode 100644 src/test/resources/schema/issue936.json 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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 1e9f23be8..66f795346 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -61,10 +61,38 @@ 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; + SchemaLocation result = schemaLocation.resolve(resolve); + 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(); diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 3656285c2..203f7d9b7 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) { 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/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 22f9f9c62..78084a81d 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -29,6 +29,10 @@ import java.util.Objects; public class SchemaValidatorsConfig { + /** + * Used to validate the acceptable $id values. + */ + private JsonSchemaIdValidator schemaIdValidator = JsonSchemaIdValidator.DEFAULT; /** * when validate type, if TYPE_LOOSE = true, will try to convert string to @@ -545,4 +549,21 @@ public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { this.formatAssertionsEnabled = formatAssertionsEnabled; } + /** + * Gets the schema id validator to validate $id. + * + * @return the validator + */ + public JsonSchemaIdValidator getSchemaIdValidator() { + return schemaIdValidator; + } + + /** + * Sets the schema id validator to validate $id. + * + * @param schemaIdValidator the validator + */ + public void setSchemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { + this.schemaIdValidator = schemaIdValidator; + } } diff --git a/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java b/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java new file mode 100644 index 000000000..cac5ef937 --- /dev/null +++ b/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java @@ -0,0 +1,70 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Tests for the non-standard DefaultJsonSchemaIdValidator. + */ +public class DefaultJsonSchemaIdValidatorTest { + @Test + void givenRelativeIdShouldThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + try { + JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config); + } catch (InvalidSchemaException e) { + assertEquals("/$id: # is an invalid segment for URI 0", e.getMessage()); + } + } + + @Test + void givenFragmentWithNoContextShouldNotThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"#0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + } + + @Test + void givenSlashWithNoContextShouldNotThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"/base\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + } + + @Test + void givenRelativeIdWithClasspathBaseShouldNotThrowInvalidSchemaException() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012) + .getSchema(SchemaLocation.of("classpath:schema/id-relative.json"), config)); + } +} diff --git a/src/test/java/com/networknt/schema/Issue347Test.java b/src/test/java/com/networknt/schema/Issue347Test.java index 331ba9fb3..d8301e20f 100644 --- a/src/test/java/com/networknt/schema/Issue347Test.java +++ b/src/test/java/com/networknt/schema/Issue347Test.java @@ -3,6 +3,7 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -11,11 +12,12 @@ public class Issue347Test { @Test public void failure() { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); + assertThrows(JsonSchemaException.class, () -> factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json"))); try { - JsonSchema schema = factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json")); + factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json")); } catch (Throwable e) { assertThat(e, instanceOf(JsonSchemaException.class)); - assertEquals("/$id: null is an invalid segment for URI test", e.getMessage()); + assertEquals("/$id: # is an invalid segment for URI test", e.getMessage()); } } } diff --git a/src/test/java/com/networknt/schema/Issue936Test.java b/src/test/java/com/networknt/schema/Issue936Test.java new file mode 100644 index 000000000..f5c72cc11 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue936Test.java @@ -0,0 +1,40 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue936Test { + @Test + void shouldThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + try { + JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config); + } catch (InvalidSchemaException e) { + assertEquals("/$id: # is an invalid segment for URI 0", e.getMessage()); + } + } +} diff --git a/src/test/resources/schema/id-relative.json b/src/test/resources/schema/id-relative.json new file mode 100644 index 000000000..d159f7ba7 --- /dev/null +++ b/src/test/resources/schema/id-relative.json @@ -0,0 +1,4 @@ +{ + "$id": "0", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} \ No newline at end of file diff --git a/src/test/resources/schema/issue936.json b/src/test/resources/schema/issue936.json new file mode 100644 index 000000000..d159f7ba7 --- /dev/null +++ b/src/test/resources/schema/issue936.json @@ -0,0 +1,4 @@ +{ + "$id": "0", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} \ No newline at end of file From 0b279d3f91ee630e6277d2dc398a1db60eec4956 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:08:05 +0800 Subject: [PATCH 25/53] Fix 935 --- .../networknt/schema/JsonSchemaFactory.java | 32 +++++++++++-------- .../com/networknt/schema/Issue935Test.java | 31 ++++++++++++++++++ 2 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 src/test/java/com/networknt/schema/Issue935Test.java diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 203f7d9b7..596332c7b 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -353,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(); } /** diff --git a/src/test/java/com/networknt/schema/Issue935Test.java b/src/test/java/com/networknt/schema/Issue935Test.java new file mode 100644 index 000000000..fdc018609 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue935Test.java @@ -0,0 +1,31 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue935Test { + @Test + void shouldThrowInvalidSchemaException() { + String schema = "{ \"$schema\": \"0\" }"; + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V201909).getSchema(schema)); + } +} From aadf9dc31f3e07761fb1419357eb21a1418ea6be Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:56:55 +0800 Subject: [PATCH 26/53] Fix --- src/main/java/com/networknt/schema/NonValidationKeyword.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index ef1b8d9f5..194150a20 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -34,7 +34,8 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso super(schemaLocation, evaluationPath, keyword); 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); } From 476fefd2d729443f4a9f395d11977c64918b9a20 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:49:27 +0800 Subject: [PATCH 27/53] Add convenience method for schema loader --- .../schema/resource/MapSchemaLoader.java | 38 +++++++++++- .../schema/resource/SchemaLoaders.java | 18 ++++++ .../schema/resource/MapSchemaLoaderTest.java | 61 +++++++++++++++++++ 3 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java diff --git a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java index 86b4633ac..8fb04eabf 100644 --- a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java @@ -12,15 +12,47 @@ */ public class MapSchemaLoader implements SchemaLoader { private final Function mappings; - + + /** + * Sets the schema data by absolute IRI. + * + * @param mappings + */ public MapSchemaLoader(Map mappings) { this(mappings::get); } - + + /** + * Sets the schema data by absolute IRI function. + * + * @param mappings the mappings + */ public MapSchemaLoader(Function mappings) { this.mappings = mappings; } - + + /** + * Sets the schema data by using two mapping functions. + *

+ * Firstly to map the IRI to an object. If the object is null no mapping is + * performed. + *

+ * Next to map the object to the schema data. + * + * @param the type of the object + * @param mapIriToObject the mapping of IRI to object + * @param mapObjectToData the mappingof object to schema data + */ + public MapSchemaLoader(Function mapIriToObject, Function mapObjectToData) { + this.mappings = iri -> { + T result = mapIriToObject.apply(iri); + if (result != null) { + return mapObjectToData.apply(result); + } + return null; + }; + } + @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { try { diff --git a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java index 607c4250c..dbe847c6d 100644 --- a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java @@ -105,6 +105,24 @@ public Builder schemas(Function schemas) { return this; } + /** + * Sets the schema data by using two mapping functions. + *

+ * Firstly to map the IRI to an object. If the object is null no mapping is + * performed. + *

+ * Next to map the object to the schema data. + * + * @param the type of the object + * @param mapIriToObject the mapping of IRI to object + * @param mapObjectToData the mappingof object to schema data + * @return the builder + */ + public Builder schemas(Function mapIriToObject, Function mapObjectToData) { + this.values.add(new MapSchemaLoader(mapIriToObject, mapObjectToData)); + return this; + } + /** * Builds a {@link SchemaLoaders}. * diff --git a/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java b/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java new file mode 100644 index 000000000..a3178d237 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java @@ -0,0 +1,61 @@ +/* + * 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.resource; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.AbsoluteIri; + +class MapSchemaLoaderTest { + public static class Result { + private final String schema; + + public Result(String schema) { + this.schema = schema; + } + + public String getSchema() { + return this.schema; + } + } + + @Test + void testMappingsWithTwoFunctions() throws IOException { + Map mappings = new HashMap<>(); + mappings.put("http://www.example.org/test.json", new Result("test")); + mappings.put("http://www.example.org/hello.json", new Result("hello")); + + MapSchemaLoader loader = new MapSchemaLoader(mappings::get, Result::getSchema); + InputStreamSource source = loader.getSchema(AbsoluteIri.of("http://www.example.org/test.json")); + try (InputStream inputStream = source.getInputStream()) { + byte[] r = new byte[4]; + inputStream.read(r); + String value = new String(r, StandardCharsets.UTF_8); + assertEquals("test", value); + } + + InputStreamSource result = loader.getSchema(AbsoluteIri.of("http://www.example.org/not-found.json")); + assertNull(result); + } +} From fa63d192df20ef391ea4225ed1c2294833077ee7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:11:52 +0800 Subject: [PATCH 28/53] Add convenience method for schema mappers --- .../schema/resource/MapSchemaMapper.java | 20 +++++++++-- .../schema/resource/SchemaMappers.java | 13 +++++++ .../schema/resource/MapSchemaMapperTest.java | 36 +++++++++++++++++++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java diff --git a/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java index 1e7b34f2b..a5e8825c9 100644 --- a/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java @@ -2,6 +2,7 @@ import java.util.Map; import java.util.function.Function; +import java.util.function.Predicate; import com.networknt.schema.AbsoluteIri; @@ -14,11 +15,26 @@ public class MapSchemaMapper implements SchemaMapper { public MapSchemaMapper(Map mappings) { this(mappings::get); } - + public MapSchemaMapper(Function mappings) { this.mappings = mappings; } - + + /** + * Apply the mapping function if the predicate is true. + * + * @param test the predicate + * @param mappings the mapping + */ + public MapSchemaMapper(Predicate test, Function mappings) { + this.mappings = iri -> { + if (test.test(iri)) { + return mappings.apply(iri); + } + return null; + }; + } + @Override public AbsoluteIri map(AbsoluteIri absoluteIRI) { String mapped = this.mappings.apply(absoluteIRI.toString()); diff --git a/src/main/java/com/networknt/schema/resource/SchemaMappers.java b/src/main/java/com/networknt/schema/resource/SchemaMappers.java index 2b0905943..1bdc79e2d 100644 --- a/src/main/java/com/networknt/schema/resource/SchemaMappers.java +++ b/src/main/java/com/networknt/schema/resource/SchemaMappers.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; /** * Schema Mappers used to map an ID indicated by an absolute IRI to a retrieval @@ -118,6 +119,18 @@ public Builder mappings(Function mappings) { return this; } + /** + * Sets the function that maps the IRI to another IRI if the predicate is true. + * + * @param test the predicate + * @param mappings the mappings + * @return the builder + */ + public Builder mappings(Predicate test, Function mappings) { + this.values.add(new MapSchemaMapper(test, mappings)); + return this; + } + /** * Builds a {@link SchemaMappers} * diff --git a/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java b/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java new file mode 100644 index 000000000..d9697b6b1 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java @@ -0,0 +1,36 @@ +/* + * 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.resource; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.AbsoluteIri; + +class MapSchemaMapperTest { + + @Test + void predicateMapping() { + MapSchemaMapper mapper = new MapSchemaMapper(test -> test.startsWith("http://www.example.org/"), + original -> original.replaceFirst("http://www.example.org/", "classpath:")); + AbsoluteIri result = mapper.map(AbsoluteIri.of("http://www.example.org/hello")); + assertEquals("classpath:hello", result.toString()); + result = mapper.map(AbsoluteIri.of("notmatchingprefixhttp://www.example.org/hello")); + assertNull(result); + } + +} From 1a27aeeca0ee06fdbe4c3244b59f388329a9a09d Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 14:54:18 +0800 Subject: [PATCH 29/53] Support annotation collection for reporting --- .../schema/AbstractJsonValidator.java | 48 +++++++- .../schema/AdditionalPropertiesValidator.java | 19 +-- .../networknt/schema/BaseJsonValidator.java | 35 ++++++ .../networknt/schema/ContainsValidator.java | 43 ++++--- .../schema/ContentEncodingValidator.java | 6 + .../schema/ContentMediaTypeValidator.java | 5 + .../com/networknt/schema/ExecutionConfig.java | 6 +- .../com/networknt/schema/ItemsValidator.java | 5 +- .../schema/ItemsValidator202012.java | 2 +- .../schema/NonValidationKeyword.java | 9 +- .../schema/PatternPropertiesValidator.java | 5 +- .../schema/PrefixItemsValidator.java | 2 +- .../networknt/schema/PropertiesValidator.java | 2 +- .../networknt/schema/output/OutputUnit.java | 112 ++++++++++++++++++ .../schema/CollectorContextTest.java | 12 +- .../schema/CustomMetaSchemaTest.java | 7 +- .../com/networknt/schema/JsonWalkTest.java | 6 +- 17 files changed, 279 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/networknt/schema/output/OutputUnit.java diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index 75f84623b..2da3983d5 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -16,15 +16,22 @@ package com.networknt.schema; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; + public abstract class AbstractJsonValidator implements JsonValidator { private final SchemaLocation schemaLocation; + private final JsonNode schemaNode; private final JsonNodePath evaluationPath; private final Keyword keyword; - public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword) { + public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword, JsonNode schemaNode) { this.schemaLocation = schemaLocation; this.evaluationPath = evaluationPath; this.keyword = keyword; + this.schemaNode = schemaNode; } @Override @@ -42,8 +49,47 @@ public String getKeyword() { return keyword.getValue(); } + public JsonNode getSchemaNode() { + return this.schemaNode; + } + @Override public String toString() { return getEvaluationPath().getName(-1); } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext) { + return collectAnnotations(executionContext, getKeyword()); + } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @param keyword the keyword + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { + return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() + && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + } + + /** + * Puts an annotation. + * + * @param executionContext the execution context + * @param customizer to customize the annotation + */ + protected void putAnnotation(ExecutionContext executionContext, Consumer customizer) { + JsonNodeAnnotation.Builder builder = JsonNodeAnnotation.builder().evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()); + customizer.accept(builder); + executionContext.getAnnotations().put(builder.build()); + } } diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 768549bb6..e7decbba1 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -74,10 +74,15 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } - Set matchedInstancePropertyNames = new LinkedHashSet<>(); + Set matchedInstancePropertyNames = null; + + boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); // if allowAdditionalProperties is true, add all the properties as evaluated. - if (allowAdditionalProperties) { + if (allowAdditionalProperties && collectAnnotations) { for (Iterator it = node.fieldNames(); it.hasNext();) { + if (matchedInstancePropertyNames == null) { + matchedInstancePropertyNames = new LinkedHashSet<>(); + } String fieldName = it.next(); matchedInstancePropertyNames.add(fieldName); } @@ -132,11 +137,11 @@ 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()); + if (collectAnnotations) { + executionContext.getAnnotations().put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation).keyword(getKeyword()) + .value(matchedInstancePropertyNames != null ? matchedInstancePropertyNames : Collections.emptySet()) + .build()); } return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 0890274fb..659318d75 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.i18n.DefaultMessageSource; import org.slf4j.Logger; @@ -26,6 +27,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; public abstract class BaseJsonValidator extends ValidationMessageHandler implements JsonValidator { protected final boolean suppressSubSchemaRetrieval; @@ -372,5 +374,38 @@ protected MessageSourceValidationMessage.Builder message() { return super.message().schemaNode(this.schemaNode); } + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext) { + return collectAnnotations(executionContext, getKeyword()); + } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @param keyword the keyword + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { + return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() + && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + } + /** + * Puts an annotation. + * + * @param executionContext the execution context + * @param customizer to customize the annotation + */ + protected void putAnnotation(ExecutionContext executionContext, Consumer customizer) { + JsonNodeAnnotation.Builder builder = JsonNodeAnnotation.builder().evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()); + customizer.accept(builder); + executionContext.getAnnotations().put(builder.build()); + } } diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index aa54ff9dd..64806d59d 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -100,15 +100,17 @@ public Set validate(ExecutionContext executionContext, JsonNo executionContext.getExecutionConfig().isFailFast(), node, instanceLocation, this.max); } } - - if (collectAnnotations()) { - if (this.schema != null) { - // This keyword produces an annotation value which is an array of the indexes to - // which this keyword validates successfully when applying its subschema, in - // ascending order. The value MAY be a boolean "true" if the subschema validates - // successfully when applied to every index of the instance. The annotation MUST - // be present if the instance array to which this keyword's schema applies is - // empty. + + boolean collectAnnotations = collectAnnotations(); + if (this.schema != null) { + // This keyword produces an annotation value which is an array of the indexes to + // which this keyword validates successfully when applying its subschema, in + // ascending order. The value MAY be a boolean "true" if the subschema validates + // successfully when applied to every index of the instance. The annotation MUST + // be present if the instance array to which this keyword's schema applies is + // empty. + + if (collectAnnotations || collectAnnotations(executionContext, "contains")) { if (actual == i) { // evaluated all executionContext.getAnnotations() @@ -121,20 +123,26 @@ public Set validate(ExecutionContext executionContext, JsonNo .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) .keyword("contains").value(indexes).build()); } - // Add minContains and maxContains annotations - if (this.min != null) { + } + + // Add minContains and maxContains annotations + if (this.min != null) { + String minContainsKeyword = "minContains"; + if (collectAnnotations || collectAnnotations(executionContext, minContainsKeyword)) { // Omitted keywords MUST NOT produce annotation results. However, as described // in the section for contains, the absence of this keyword's annotation causes // contains to assume a minimum value of 1. - String minContainsKeyword = "minContains"; executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath.append(minContainsKeyword)) .schemaLocation(this.schemaLocation.append(minContainsKeyword)) .keyword(minContainsKeyword).value(this.min).build()); } - if (this.max != null) { - String maxContainsKeyword = "maxContains"; + } + + if (this.max != null) { + String maxContainsKeyword = "maxContains"; + if (collectAnnotations || collectAnnotations(executionContext, maxContainsKeyword)) { executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath.append(maxContainsKeyword)) @@ -166,6 +174,13 @@ private Set boundsViolated(ValidatorTypeCode validatorTypeCod .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(); } diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java index 3318bda7e..addb0dbfb 100644 --- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -17,6 +17,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -64,6 +65,11 @@ 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().instanceNode(node).instanceLocation(instanceLocation) diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java index a7e2cc405..8e21cfdeb 100644 --- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -89,6 +89,11 @@ 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().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index 6faf7ea45..f1a04569b 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -43,7 +43,7 @@ public class ExecutionConfig { * This does not affect annotation collection required for evaluating keywords * such as unevaluatedItems or unevaluatedProperties and only affects reporting. */ - private Predicate annotationCollectionPredicate = (keyword) -> true; + private Predicate annotationCollectionPredicate = keyword -> false; /** * Since Draft 2019-09 format assertions are not enabled by default. @@ -150,7 +150,7 @@ protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnable * particular keyword. This only has an effect if annotation collection is * enabled. *

- * The default value is to collect all annotation keywords if annotation + * 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 @@ -167,7 +167,7 @@ public Predicate getAnnotationCollectionPredicate() { * 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 collect all annotation keywords if annotation + * 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 diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 1282ce0a0..ce23f1faa 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -81,9 +81,10 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays return Collections.emptySet(); } + boolean collectAnnotations = collectAnnotations(); // Add items annotation - if (collectAnnotations()) { + if (collectAnnotations || collectAnnotations(executionContext)) { if (this.schema != null) { // Applies to all executionContext.getAnnotations() @@ -127,7 +128,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (hasAdditionalItem) { - if (collectAnnotations()) { + if (collectAnnotations || collectAnnotations(executionContext, "additionalItems")) { executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 443621458..f94783578 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -78,7 +78,7 @@ public Set validate(ExecutionContext executionContext, JsonNo evaluated = true; } if (evaluated) { - if (collectAnnotations()) { + if (collectAnnotations() || collectAnnotations(executionContext)) { // Applies to all executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index 194150a20..ce0a178d1 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -31,7 +31,7 @@ public class NonValidationKeyword extends AbstractKeyword { private static final class Validator extends AbstractJsonValidator { public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { - super(schemaLocation, evaluationPath, keyword); + super(schemaLocation, evaluationPath, keyword, schemaNode); String id = validationContext.resolveSchemaId(schemaNode); String anchor = validationContext.getMetaSchema().readAnchor(schemaNode); String dynamicAnchor = validationContext.getMetaSchema().readDynamicAnchor(schemaNode); @@ -50,6 +50,13 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + if (collectAnnotations(executionContext)) { + if (getSchemaNode().isTextual()) { + String value = getSchemaNode().textValue(); + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(value)); + } + } return Collections.emptySet(); } } diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index 17921ee0b..08d945c47 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -55,6 +55,7 @@ public Set validate(ExecutionContext executionContext, JsonNo 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); @@ -63,7 +64,7 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNodePath path = instanceLocation.append(name); Set results = entry.getValue().validate(executionContext, n, rootNode, path); if (results.isEmpty()) { - if (collectAnnotations()) { + if (collectAnnotations) { if (matchedInstancePropertyNames == null) { matchedInstancePropertyNames = new LinkedHashSet<>(); } @@ -78,7 +79,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } - if (collectAnnotations()) { + if (collectAnnotations) { executionContext.getAnnotations() .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 922aafc09..27890b93a 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -70,7 +70,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } // Add annotation - if (collectAnnotations()) { + if (collectAnnotations() || collectAnnotations(executionContext)) { // Tuples int items = node.isArray() ? node.size() : 1; int schemas = this.tupleSchema.size(); diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index bac7522f3..a5fc1171b 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -56,7 +56,7 @@ public Set validate(ExecutionContext executionContext, JsonNo Set requiredErrors = null; Set matchedInstancePropertyNames = null; - boolean collectAnnotations = collectAnnotations(); + boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); for (Map.Entry entry : this.schemas.entrySet()) { JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = node.get(entry.getKey()); diff --git a/src/main/java/com/networknt/schema/output/OutputUnit.java b/src/main/java/com/networknt/schema/output/OutputUnit.java new file mode 100644 index 000000000..7bb0d587a --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnit.java @@ -0,0 +1,112 @@ +/* + * 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.output; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +/** + * Represents an output unit. + * + * @see A + * Specification for Machine-Readable Output for JSON Schema Validation and + * Annotation + */ +@JsonInclude(Include.NON_NULL) +public class OutputUnit { + private boolean valid; + + private String evaluationPath = null; + private String schemaLocation = null; + private String instanceLocation = null; + + private Map errors = null; + + private Map annotations = null; + + private Map droppedAnnotations = null; + + private List details = null; + + protected boolean isValid() { + return valid; + } + + protected void setValid(boolean valid) { + this.valid = valid; + } + + protected String getEvaluationPath() { + return evaluationPath; + } + + protected void setEvaluationPath(String evaluationPath) { + this.evaluationPath = evaluationPath; + } + + protected String getSchemaLocation() { + return schemaLocation; + } + + protected void setSchemaLocation(String schemaLocation) { + this.schemaLocation = schemaLocation; + } + + protected String getInstanceLocation() { + return instanceLocation; + } + + protected void setInstanceLocation(String instanceLocation) { + this.instanceLocation = instanceLocation; + } + + protected Map getErrors() { + return errors; + } + + protected void setErrors(Map errors) { + this.errors = errors; + } + + protected Map getAnnotations() { + return annotations; + } + + protected void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + protected Map getDroppedAnnotations() { + return droppedAnnotations; + } + + protected void setDroppedAnnotations(Map droppedAnnotations) { + this.droppedAnnotations = droppedAnnotations; + } + + protected List getDetails() { + return details; + } + + protected void setDetails(List details) { + this.details = details; + } + +} diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java index 779c27273..a7956b9b8 100644 --- a/src/test/java/com/networknt/schema/CollectorContextTest.java +++ b/src/test/java/com/networknt/schema/CollectorContextTest.java @@ -255,7 +255,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(schemaLocation, evaluationPath); + return new CustomValidator(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -268,8 +268,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev * document again just for gathering this kind of information. */ private class CustomValidator extends AbstractJsonValidator { - public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new CustomKeyword(), schemaNode); } @Override @@ -326,7 +326,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator1(schemaLocation, evaluationPath); + return new CustomValidator1(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -341,8 +341,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev * keyword has been used multiple times in JSON Schema. */ private class CustomValidator1 extends AbstractJsonValidator { - public CustomValidator1(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator1(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath,new CustomKeyword(), schemaNode); } @SuppressWarnings("unchecked") diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index 936d4ab4b..63d84a1e1 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -49,8 +49,8 @@ private static final class Validator extends AbstractJsonValidator { private final String keyword; private Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword, - List enumValues, List enumNames) { - super(schemaLocation, evaluationPath,null); + List enumValues, List enumNames, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new EnumNamesKeyword(), schemaNode); if (enumNames.size() != enumValues.size()) { throw new IllegalArgumentException("enum and enumNames need to be of same length"); } @@ -98,7 +98,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev } JsonNode enumSchemaNode = parentSchemaNode.get("enum"); - return new Validator(schemaLocation, evaluationPath, getValue(), readStringList(enumSchemaNode), readStringList(schemaNode)); + return new Validator(schemaLocation, evaluationPath, getValue(), readStringList(enumSchemaNode), + readStringList(schemaNode), schemaNode); } private List readStringList(JsonNode node) { diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java index 50a14608b..788dcd79a 100644 --- a/src/test/java/com/networknt/schema/JsonWalkTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkTest.java @@ -127,7 +127,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(schemaLocation, evaluationPath); + return new CustomValidator(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -140,8 +140,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev */ private static class CustomValidator extends AbstractJsonValidator { - public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new CustomKeyword(), schemaNode); } @Override From 8b779f6e6665e5afec889aa329bf9a1152d5c784 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 16:36:49 +0800 Subject: [PATCH 30/53] Support output formatting --- .../com/networknt/schema/Annotations.java | 100 ---------- .../schema/NonValidationKeyword.java | 23 ++- .../com/networknt/schema/OutputFormat.java | 54 +++++- .../HierarchicalOutputUnitFormatter.java | 32 ++++ .../output/ListOutputUnitFormatter.java | 180 ++++++++++++++++++ .../networknt/schema/output/OutputUnit.java | 35 ++-- .../schema/ContentSchemaValidatorTest.java | 104 ++++++++++ .../com/networknt/schema/OutputUnitTest.java | 166 ++++++++++++++++ 8 files changed, 567 insertions(+), 127 deletions(-) delete mode 100644 src/main/java/com/networknt/schema/Annotations.java create mode 100644 src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java create mode 100644 src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java create mode 100644 src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java create mode 100644 src/test/java/com/networknt/schema/OutputUnitTest.java diff --git a/src/main/java/com/networknt/schema/Annotations.java b/src/main/java/com/networknt/schema/Annotations.java deleted file mode 100644 index 55a0386ca..000000000 --- a/src/main/java/com/networknt/schema/Annotations.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.networknt.schema; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Predicate; - -/** - * Annotations. - */ -public class Annotations { - public static final Set UNEVALUATED_PROPERTIES_ANNOTATIONS; - public static final Set UNEVALUATED_ITEMS_ANNOTATIONS; - public static final Set EVALUATION_ANNOTATIONS; - - public static final Predicate UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; - public static final Predicate UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; - public static final Predicate EVALUATION_ANNOTATIONS_PREDICATE; - public static final Predicate PREDICATE_FALSE; - - static { - Set unevaluatedProperties = new HashSet<>(); - unevaluatedProperties.add("unevaluatedProperties"); - unevaluatedProperties.add("properties"); - unevaluatedProperties.add("patternProperties"); - unevaluatedProperties.add("additionalProperties"); - UNEVALUATED_PROPERTIES_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedProperties); - - Set unevaluatedItems = new HashSet<>(); - unevaluatedItems.add("unevaluatedItems"); - unevaluatedItems.add("items"); - unevaluatedItems.add("prefixItems"); - unevaluatedItems.add("additionalItems"); - unevaluatedItems.add("contains"); - UNEVALUATED_ITEMS_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedItems); - - Set evaluation = new HashSet<>(); - evaluation.addAll(unevaluatedProperties); - evaluation.addAll(unevaluatedItems); - EVALUATION_ANNOTATIONS = Collections.unmodifiableSet(evaluation); - - UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE = UNEVALUATED_PROPERTIES_ANNOTATIONS::contains; - UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE = UNEVALUATED_ITEMS_ANNOTATIONS::contains; - EVALUATION_ANNOTATIONS_PREDICATE = EVALUATION_ANNOTATIONS::contains; - PREDICATE_FALSE = (keyword) -> false; - } - - /** - * Gets the default annotation allow list. - * - * @param metaSchema the meta schema - * @return the default annotation allow set - */ - public static Set getDefaultAnnotationAllowList(JsonMetaSchema metaSchema) { - boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; - boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; - if (unevaluatedProperties && unevaluatedItems) { - return EVALUATION_ANNOTATIONS; - } else if (unevaluatedProperties && !unevaluatedItems) { - return UNEVALUATED_PROPERTIES_ANNOTATIONS; - } else if (!unevaluatedProperties && unevaluatedItems) { - return UNEVALUATED_ITEMS_ANNOTATIONS; - } - return Collections.emptySet(); - } - - /** - * Gets the default annotation allow list predicate. - * - * @param metaSchema the meta schema - * @return the default annotation allow list predicate - */ - public static Predicate getDefaultAnnotationAllowListPredicate(JsonMetaSchema metaSchema) { - boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; - boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; - if (unevaluatedProperties && unevaluatedItems) { - return EVALUATION_ANNOTATIONS_PREDICATE; - } else if (unevaluatedProperties && !unevaluatedItems) { - return UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; - } else if (!unevaluatedProperties && unevaluatedItems) { - return UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; - } - return PREDICATE_FALSE; - } -} diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index ce0a178d1..81021a59e 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -50,15 +50,28 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (collectAnnotations(executionContext)) { - if (getSchemaNode().isTextual()) { - String value = getSchemaNode().textValue(); - putAnnotation(executionContext, - annotation -> annotation.instanceLocation(instanceLocation).value(value)); + if (!("$defs".equals(getKeyword()) || "definitions".equals(getKeyword()) || getKeyword().startsWith("$"))) { + if (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) { diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java index d798c344d..a1aa9b24b 100644 --- a/src/main/java/com/networknt/schema/OutputFormat.java +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -17,6 +17,10 @@ import java.util.Set; +import com.networknt.schema.output.HierarchicalOutputUnitFormatter; +import com.networknt.schema.output.ListOutputUnitFormatter; +import com.networknt.schema.output.OutputUnit; + /** * Formats the validation results. * @@ -59,6 +63,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,8 +81,7 @@ T format(Set validationMessages, ExecutionContext executionCo public static class Default implements OutputFormat> { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationCollectionPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); } @Override @@ -83,8 +97,7 @@ public Set format(Set validationMessages, public static class Flag implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationCollectionPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); } @Override @@ -100,8 +113,7 @@ public FlagOutput format(Set validationMessages, ExecutionCon public static class Boolean implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationCollectionPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); } @Override @@ -110,6 +122,36 @@ public java.lang.Boolean format(Set validationMessages, Execu return validationMessages.isEmpty(); } } + + /** + * The List output format. + */ + public static class List implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + } + + @Override + public OutputUnit format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext); + } + } + + /** + * The Hierarchical output format. + */ + public static class Hierarchical implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + } + + @Override + public OutputUnit format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + return HierarchicalOutputUnitFormatter.format(validationMessages, executionContext, validationContext); + } + } /** * The Flag output results. diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java new file mode 100644 index 000000000..5e23b8725 --- /dev/null +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.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.output; + +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.ValidationContext; +import com.networknt.schema.ValidationMessage; + +/** + * HierarchicalOutputUnitFormatter. + */ +public class HierarchicalOutputUnitFormatter { + public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + return null; + } +} diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java new file mode 100644 index 000000000..d809511ea --- /dev/null +++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java @@ -0,0 +1,180 @@ +/* + * 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.output; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.ValidationContext; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.annotation.JsonNodeAnnotation; + +/** + * ListOutputUnitFormatter. + */ +public class ListOutputUnitFormatter { + public static class Key { + private final String evaluationPath; + private final String schemaLocation; + private final String instanceLocation; + + public Key(String evaluationPath, String schemaLocation, String instanceLocation) { + super(); + this.evaluationPath = evaluationPath; + this.schemaLocation = schemaLocation; + this.instanceLocation = instanceLocation; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, schemaLocation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Key other = (Key) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation); + } + } + + public static String formatMessage(String message) { + int index = message.indexOf(": "); + if (index != -1) { + return message.substring(index + 2); + } + return message; + } + + public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + OutputUnit root = new OutputUnit(); + root.setValid(validationMessages.isEmpty()); + + Map valid = new LinkedHashMap<>(); + Map> errors = new HashMap<>(); + Map> annotations = new HashMap<>(); + Map> droppedAnnotations = new HashMap<>(); + + for (ValidationMessage assertion : validationMessages) { + SchemaLocation assertionSchemaLocation = new SchemaLocation(assertion.getSchemaLocation().getAbsoluteIri(), + assertion.getSchemaLocation().getFragment().getParent()); + Key key = new Key(assertion.getEvaluationPath().getParent().toString(), assertionSchemaLocation.toString(), + assertion.getInstanceLocation().toString()); + valid.put(key, false); + Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); + errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); + } + + for (List annotationsResult : executionContext.getAnnotations().asMap().values()) { + for (JsonNodeAnnotation annotation : annotationsResult) { + // As some annotations are required for computation, filter those that are not + // required for reporting + if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate() + .test(annotation.getKeyword())) { + SchemaLocation annotationSchemaLocation = new SchemaLocation( + annotation.getSchemaLocation().getAbsoluteIri(), + annotation.getSchemaLocation().getFragment().getParent()); + + Key key = new Key(annotation.getEvaluationPath().getParent().toString(), + annotationSchemaLocation.toString(), annotation.getInstanceLocation().toString()); + boolean validResult = executionContext.getResults().isValid(annotation.getInstanceLocation(), + annotation.getEvaluationPath()); + valid.put(key, validResult); + if (validResult) { + // annotations + Map annotationMap = annotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + annotationMap.put(annotation.getKeyword(), annotation.getValue()); + } else { + // dropped annotations + Map droppedAnnotationMap = droppedAnnotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + droppedAnnotationMap.put(annotation.getKeyword(), annotation.getValue()); + } + } + } + } + + // Process the list + for (Entry entry : valid.entrySet()) { + OutputUnit output = new OutputUnit(); + Key key = entry.getKey(); + output.setValid(entry.getValue()); + output.setEvaluationPath(key.evaluationPath); + output.setSchemaLocation(key.schemaLocation); + output.setInstanceLocation(key.instanceLocation); + + // Errors + Map errorMap = errors.get(key); + if (errorMap != null && !errorMap.isEmpty()) { + if (output.getErrors() == null) { + output.setErrors(new LinkedHashMap<>()); + } + for (Entry errorEntry : errorMap.entrySet()) { + output.getErrors().put(errorEntry.getKey(), errorEntry.getValue()); + } + } + + // Annotations + Map annotationsMap = annotations.get(key); + if (annotationsMap != null && !annotationsMap.isEmpty()) { + if (output.getAnnotations() == null) { + output.setAnnotations(new LinkedHashMap<>()); + } + for (Entry annotationEntry : annotationsMap.entrySet()) { + output.getAnnotations().put(annotationEntry.getKey(), annotationEntry.getValue()); + } + } + + // Dropped Annotations + Map droppedAnnotationsMap = droppedAnnotations.get(key); + if (droppedAnnotationsMap != null && !droppedAnnotationsMap.isEmpty()) { + if (output.getDroppedAnnotations() == null) { + output.setDroppedAnnotations(new LinkedHashMap<>()); + } + for (Entry droppedAnnotationEntry : droppedAnnotationsMap.entrySet()) { + output.getDroppedAnnotations().put(droppedAnnotationEntry.getKey(), + droppedAnnotationEntry.getValue()); + } + } + + List details = root.getDetails(); + if (details == null) { + details = new ArrayList<>(); + root.setDetails(details); + } + details.add(output); + } + + return root; + } +} diff --git a/src/main/java/com/networknt/schema/output/OutputUnit.java b/src/main/java/com/networknt/schema/output/OutputUnit.java index 7bb0d587a..1e12ba87c 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnit.java +++ b/src/main/java/com/networknt/schema/output/OutputUnit.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; /** * Represents an output unit. @@ -30,6 +31,8 @@ * Annotation */ @JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({ "valid", "evaluationPath", "schemaLocation", "instanceLocation", "errors", "annotations", + "droppedAnnotations", "details" }) public class OutputUnit { private boolean valid; @@ -45,67 +48,67 @@ public class OutputUnit { private List details = null; - protected boolean isValid() { + public boolean isValid() { return valid; } - protected void setValid(boolean valid) { + public void setValid(boolean valid) { this.valid = valid; } - protected String getEvaluationPath() { + public String getEvaluationPath() { return evaluationPath; } - protected void setEvaluationPath(String evaluationPath) { + public void setEvaluationPath(String evaluationPath) { this.evaluationPath = evaluationPath; } - protected String getSchemaLocation() { + public String getSchemaLocation() { return schemaLocation; } - protected void setSchemaLocation(String schemaLocation) { + public void setSchemaLocation(String schemaLocation) { this.schemaLocation = schemaLocation; } - protected String getInstanceLocation() { + public String getInstanceLocation() { return instanceLocation; } - protected void setInstanceLocation(String instanceLocation) { + public void setInstanceLocation(String instanceLocation) { this.instanceLocation = instanceLocation; } - protected Map getErrors() { + public Map getErrors() { return errors; } - protected void setErrors(Map errors) { + public void setErrors(Map errors) { this.errors = errors; } - protected Map getAnnotations() { + public Map getAnnotations() { return annotations; } - protected void setAnnotations(Map annotations) { + public void setAnnotations(Map annotations) { this.annotations = annotations; } - protected Map getDroppedAnnotations() { + public Map getDroppedAnnotations() { return droppedAnnotations; } - protected void setDroppedAnnotations(Map droppedAnnotations) { + public void setDroppedAnnotations(Map droppedAnnotations) { this.droppedAnnotations = droppedAnnotations; } - protected List getDetails() { + public List getDetails() { return details; } - protected void setDetails(List details) { + public void setDetails(List details) { this.details = details; } diff --git a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java new file mode 100644 index 000000000..86bf4017d --- /dev/null +++ b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.output.OutputUnit; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * ContentSchemaValidatorTest. + */ +public class ContentSchemaValidatorTest { + @Test + void annotationCollection() throws JsonProcessingException { + String schemaData = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"contentMediaType\": \"application/jwt\",\r\n" + + " \"contentSchema\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 2,\r\n" + + " \"prefixItems\": [\r\n" + + " {\r\n" + + " \"const\": {\r\n" + + " \"typ\": \"JWT\",\r\n" + + " \"alg\": \"HS256\"\r\n" + + " }\r\n" + + " },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\"iss\", \"exp\"],\r\n" + + " \"properties\": {\r\n" + + " \"iss\": {\"type\": \"string\"},\r\n" + + " \"exp\": {\"type\": \"integer\"}\r\n" + + " }\r\n" + + " }\r\n" + + " ]\r\n" + + " }\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = "\"helloworld\""; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); + String expected = "{\r\n" + + " \"valid\" : true,\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"\",\r\n" + + " \"schemaLocation\" : \"#\",\r\n" + + " \"instanceLocation\" : \"\",\r\n" + + " \"annotations\" : {\r\n" + + " \"contentMediaType\" : \"application/jwt\",\r\n" + + " \"contentSchema\" : {\r\n" + + " \"type\" : \"array\",\r\n" + + " \"minItems\" : 2,\r\n" + + " \"prefixItems\" : [ {\r\n" + + " \"const\" : {\r\n" + + " \"typ\" : \"JWT\",\r\n" + + " \"alg\" : \"HS256\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"type\" : \"object\",\r\n" + + " \"required\" : [ \"iss\", \"exp\" ],\r\n" + + " \"properties\" : {\r\n" + + " \"iss\" : {\r\n" + + " \"type\" : \"string\"\r\n" + + " },\r\n" + + " \"exp\" : {\r\n" + + " \"type\" : \"integer\"\r\n" + + " }\r\n" + + " }\r\n" + + " } ]\r\n" + + " }\r\n" + + " }\r\n" + + " } ]\r\n" + + "}"; + assertEquals(expected, output); + } +} diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java new file mode 100644 index 000000000..0d5587ae0 --- /dev/null +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -0,0 +1,166 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.output.OutputUnit; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * OutputUnitTest. + * + * @see A + * Specification for Machine-Readable Output for JSON Schema Validation and + * Annotation + */ +public class OutputUnitTest { + String schemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$id\": \"https://json-schema.org/schemas/example\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"root\",\r\n" + + " \"properties\": {\r\n" + + " \"foo\": {\r\n" + + " \"allOf\": [\r\n" + + " { \"required\": [\"unspecified-prop\"] },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"foo-title\",\r\n" + + " \"properties\": {\r\n" + + " \"foo-prop\": {\r\n" + + " \"const\": 1,\r\n" + + " \"title\": \"foo-prop-title\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"additionalProperties\": { \"type\": \"boolean\" }\r\n" + + " }\r\n" + + " ]\r\n" + + " },\r\n" + + " \"bar\": { \"$ref\": \"#/$defs/bar\" }\r\n" + + " },\r\n" + + " \"$defs\": {\r\n" + + " \"bar\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"bar-title\",\r\n" + + " \"properties\": {\r\n" + + " \"bar-prop\": {\r\n" + + " \"type\": \"integer\",\r\n" + + " \"minimum\": 10,\r\n" + + " \"title\": \"bar-prop-title\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + String inputData1 = "{\r\n" + + " \"foo\": { \"foo-prop\": \"not 1\", \"other-prop\": false },\r\n" + + " \"bar\": { \"bar-prop\": 2 }\r\n" + + "}"; + + String inputData2 = "{\r\n" + + " \"foo\": {\r\n" + + " \"foo-prop\": 1,\r\n" + + " \"unspecified-prop\": true\r\n" + + " },\r\n" + + " \"bar\": { \"bar-prop\": 20 }\r\n" + + "}"; + @Test + void annotationCollectionList() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData1; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); + String expected = "{\r\n" + + " \"valid\" : false,\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/0\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\r\n" + + " \"instanceLocation\" : \"/foo\",\r\n" + + " \"errors\" : {\r\n" + + " \"required\" : \"required property 'unspecified-prop' not found\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" + + " \"errors\" : {\r\n" + + " \"const\" : \"must be a constant value 1\"\r\n" + + " },\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"title\" : \"foo-prop-title\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" + + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" + + " \"errors\" : {\r\n" + + " \"minimum\" : \"must have a minimum value of 10\"\r\n" + + " },\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"title\" : \"bar-prop-title\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" + + " \"instanceLocation\" : \"/foo\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"foo-prop\" ],\r\n" + + " \"title\" : \"foo-title\",\r\n" + + " \"additionalProperties\" : [ \"foo-prop\", \"other-prop\" ]\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" + + " \"instanceLocation\" : \"/bar\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"bar-prop\" ],\r\n" + + " \"title\" : \"bar-title\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" + + " \"instanceLocation\" : \"\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" + + " \"title\" : \"root\"\r\n" + + " }\r\n" + + " } ]\r\n" + + "}"; + assertEquals(expected, output); + + } +} From ab2c9b378cf43535bafac7755983e0b64bb55c48 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 17:06:21 +0800 Subject: [PATCH 31/53] Refactor --- .../com/networknt/schema/AbstractKeyword.java | 15 +++++++++-- .../java/com/networknt/schema/Keyword.java | 20 +++++++++++++++ .../schema/NonValidationKeyword.java | 25 ++++++++++++------- .../com/networknt/schema/Version201909.java | 14 +++++------ .../com/networknt/schema/Version202012.java | 14 +++++------ .../java/com/networknt/schema/Version4.java | 6 ++--- .../java/com/networknt/schema/Version6.java | 6 ++--- .../java/com/networknt/schema/Version7.java | 8 +++--- 8 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/main/java/com/networknt/schema/AbstractKeyword.java b/src/main/java/com/networknt/schema/AbstractKeyword.java index 4f8feca0a..79878896c 100644 --- a/src/main/java/com/networknt/schema/AbstractKeyword.java +++ b/src/main/java/com/networknt/schema/AbstractKeyword.java @@ -16,16 +16,27 @@ package com.networknt.schema; - +/** + * Abstract keyword. + */ public abstract class AbstractKeyword implements Keyword { private final String value; + /** + * Create abstract keyword. + * + * @param value the keyword + */ public AbstractKeyword(String value) { this.value = value; } + /** + * Gets the keyword. + * + * @return the keyword + */ public String getValue() { return value; } - } 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/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index 81021a59e..c5af300b1 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -27,11 +27,15 @@ * 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) { + 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); String dynamicAnchor = validationContext.getMetaSchema().readDynamicAnchor(schemaNode); @@ -50,13 +54,11 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!("$defs".equals(getKeyword()) || "definitions".equals(getKeyword()) || getKeyword().startsWith("$"))) { - if (collectAnnotations(executionContext)) { - Object value = getAnnotationValue(getSchemaNode()); - if (value != null) { - putAnnotation(executionContext, - annotation -> annotation.instanceLocation(instanceLocation).value(value)); - } + if (collectAnnotations && collectAnnotations(executionContext)) { + Object value = getAnnotationValue(getSchemaNode()); + if (value != null) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(value)); } } return Collections.emptySet(); @@ -75,12 +77,17 @@ protected Object getAnnotationValue(JsonNode schemaNode) { } 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/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 81db41595..0972bdcb0 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -33,17 +33,17 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V201909)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$recursiveAnchor"), - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$vocabulary"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$recursiveAnchor", false), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$vocabulary", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs"), // newly added in 2019-09 release. - new NonValidationKeyword("$anchor"), + new NonValidationKeyword("$defs", false), // newly added in 2019-09 release. + new NonValidationKeyword("$anchor", false), new NonValidationKeyword("additionalItems"), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 6405fc627..4ff920741 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -35,17 +35,17 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V202012)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs"), - new NonValidationKeyword("$anchor"), - new NonValidationKeyword("$dynamicAnchor"), - new NonValidationKeyword("$vocabulary"), + new NonValidationKeyword("$defs", false), + new NonValidationKeyword("$anchor", false), + new NonValidationKeyword("$dynamicAnchor", false), + new NonValidationKeyword("$vocabulary", false), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), diff --git a/src/main/java/com/networknt/schema/Version4.java b/src/main/java/com/networknt/schema/Version4.java index 8dd86aa79..b9f886599 100644 --- a/src/main/java/com/networknt/schema/Version4.java +++ b/src/main/java/com/networknt/schema/Version4.java @@ -19,12 +19,12 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V4)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("additionalItems"), new NonValidationKeyword("exampleSetFlag") )) diff --git a/src/main/java/com/networknt/schema/Version6.java b/src/main/java/com/networknt/schema/Version6.java index 920b9520f..1f92e37eb 100644 --- a/src/main/java/com/networknt/schema/Version6.java +++ b/src/main/java/com/networknt/schema/Version6.java @@ -20,13 +20,13 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V6)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("definitions") + new NonValidationKeyword("definitions", false) )) .build(); } diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index ebf01d849..2558b343e 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -19,18 +19,18 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V7)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), new NonValidationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("message") + new NonValidationKeyword("message", false) )) .build(); } From f67103718525c87bc562eaa01c6cfcd30893603c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:54:47 +0800 Subject: [PATCH 32/53] Refactor --- .../output/ListOutputUnitFormatter.java | 102 ++-------------- .../schema/output/OutputUnitData.java | 111 ++++++++++++++++++ .../schema/output/OutputUnitKey.java | 65 ++++++++++ 3 files changed, 186 insertions(+), 92 deletions(-) create mode 100644 src/main/java/com/networknt/schema/output/OutputUnitData.java create mode 100644 src/main/java/com/networknt/schema/output/OutputUnitKey.java diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java index d809511ea..a7d2ff21d 100644 --- a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java @@ -16,122 +16,40 @@ package com.networknt.schema.output; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Objects; import java.util.Set; import com.networknt.schema.ExecutionContext; -import com.networknt.schema.SchemaLocation; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; -import com.networknt.schema.annotation.JsonNodeAnnotation; /** * ListOutputUnitFormatter. */ public class ListOutputUnitFormatter { - public static class Key { - private final String evaluationPath; - private final String schemaLocation; - private final String instanceLocation; - - public Key(String evaluationPath, String schemaLocation, String instanceLocation) { - super(); - this.evaluationPath = evaluationPath; - this.schemaLocation = schemaLocation; - this.instanceLocation = instanceLocation; - } - - @Override - public int hashCode() { - return Objects.hash(evaluationPath, instanceLocation, schemaLocation); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Key other = (Key) obj; - return Objects.equals(evaluationPath, other.evaluationPath) - && Objects.equals(instanceLocation, other.instanceLocation) - && Objects.equals(schemaLocation, other.schemaLocation); - } - } - - public static String formatMessage(String message) { - int index = message.indexOf(": "); - if (index != -1) { - return message.substring(index + 2); - } - return message; - } - public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) { OutputUnit root = new OutputUnit(); root.setValid(validationMessages.isEmpty()); - Map valid = new LinkedHashMap<>(); - Map> errors = new HashMap<>(); - Map> annotations = new HashMap<>(); - Map> droppedAnnotations = new HashMap<>(); - - for (ValidationMessage assertion : validationMessages) { - SchemaLocation assertionSchemaLocation = new SchemaLocation(assertion.getSchemaLocation().getAbsoluteIri(), - assertion.getSchemaLocation().getFragment().getParent()); - Key key = new Key(assertion.getEvaluationPath().getParent().toString(), assertionSchemaLocation.toString(), - assertion.getInstanceLocation().toString()); - valid.put(key, false); - Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); - errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); - } - - for (List annotationsResult : executionContext.getAnnotations().asMap().values()) { - for (JsonNodeAnnotation annotation : annotationsResult) { - // As some annotations are required for computation, filter those that are not - // required for reporting - if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate() - .test(annotation.getKeyword())) { - SchemaLocation annotationSchemaLocation = new SchemaLocation( - annotation.getSchemaLocation().getAbsoluteIri(), - annotation.getSchemaLocation().getFragment().getParent()); + OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); - Key key = new Key(annotation.getEvaluationPath().getParent().toString(), - annotationSchemaLocation.toString(), annotation.getInstanceLocation().toString()); - boolean validResult = executionContext.getResults().isValid(annotation.getInstanceLocation(), - annotation.getEvaluationPath()); - valid.put(key, validResult); - if (validResult) { - // annotations - Map annotationMap = annotations.computeIfAbsent(key, - k -> new LinkedHashMap<>()); - annotationMap.put(annotation.getKeyword(), annotation.getValue()); - } else { - // dropped annotations - Map droppedAnnotationMap = droppedAnnotations.computeIfAbsent(key, - k -> new LinkedHashMap<>()); - droppedAnnotationMap.put(annotation.getKeyword(), annotation.getValue()); - } - } - } - } + Map valid = data.getValid(); + Map> errors = data.getErrors(); + Map> annotations = data.getAnnotations(); + Map> droppedAnnotations = data.getDroppedAnnotations(); // Process the list - for (Entry entry : valid.entrySet()) { + for (Entry entry : valid.entrySet()) { OutputUnit output = new OutputUnit(); - Key key = entry.getKey(); + OutputUnitKey key = entry.getKey(); output.setValid(entry.getValue()); - output.setEvaluationPath(key.evaluationPath); - output.setSchemaLocation(key.schemaLocation); - output.setInstanceLocation(key.instanceLocation); + output.setEvaluationPath(key.getEvaluationPath()); + output.setSchemaLocation(key.getSchemaLocation()); + output.setInstanceLocation(key.getInstanceLocation()); // Errors Map errorMap = errors.get(key); diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java new file mode 100644 index 000000000..cffd15087 --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java @@ -0,0 +1,111 @@ +/* + * 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.output; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.annotation.JsonNodeAnnotation; + +/** + * Output Unit Data. + */ +public class OutputUnitData { + private final Map valid = new LinkedHashMap<>(); + private final Map> errors = new HashMap<>(); + private final Map> annotations = new HashMap<>(); + private final Map> droppedAnnotations = new HashMap<>(); + + public Map getValid() { + return valid; + } + + public Map> getErrors() { + return errors; + } + + public Map> getAnnotations() { + return annotations; + } + + public Map> getDroppedAnnotations() { + return droppedAnnotations; + } + + public static String formatMessage(String message) { + int index = message.indexOf(": "); + if (index != -1) { + return message.substring(index + 2); + } + return message; + } + + public static OutputUnitData from(Set validationMessages, ExecutionContext executionContext) { + OutputUnitData data = new OutputUnitData(); + + Map valid = data.valid; + Map> errors = data.errors; + Map> annotations = data.annotations; + Map> droppedAnnotations = data.droppedAnnotations; + + for (ValidationMessage assertion : validationMessages) { + SchemaLocation assertionSchemaLocation = new SchemaLocation(assertion.getSchemaLocation().getAbsoluteIri(), + assertion.getSchemaLocation().getFragment().getParent()); + OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent().toString(), + assertionSchemaLocation.toString(), assertion.getInstanceLocation().toString()); + valid.put(key, false); + Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); + errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); + } + + for (List annotationsResult : executionContext.getAnnotations().asMap().values()) { + for (JsonNodeAnnotation annotation : annotationsResult) { + // As some annotations are required for computation, filter those that are not + // required for reporting + if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate() + .test(annotation.getKeyword())) { + SchemaLocation annotationSchemaLocation = new SchemaLocation( + annotation.getSchemaLocation().getAbsoluteIri(), + annotation.getSchemaLocation().getFragment().getParent()); + + OutputUnitKey key = new OutputUnitKey(annotation.getEvaluationPath().getParent().toString(), + annotationSchemaLocation.toString(), annotation.getInstanceLocation().toString()); + boolean validResult = executionContext.getResults().isValid(annotation.getInstanceLocation(), + annotation.getEvaluationPath()); + valid.put(key, validResult); + if (validResult) { + // annotations + Map annotationMap = annotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + annotationMap.put(annotation.getKeyword(), annotation.getValue()); + } else { + // dropped annotations + Map droppedAnnotationMap = droppedAnnotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + droppedAnnotationMap.put(annotation.getKeyword(), annotation.getValue()); + } + } + } + } + return data; + } +} diff --git a/src/main/java/com/networknt/schema/output/OutputUnitKey.java b/src/main/java/com/networknt/schema/output/OutputUnitKey.java new file mode 100644 index 000000000..3c1a0ab09 --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnitKey.java @@ -0,0 +1,65 @@ +/* + * 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.output; + +import java.util.Objects; + +/** + * Output Unit Key. + */ +public class OutputUnitKey { + final String evaluationPath; + final String schemaLocation; + final String instanceLocation; + + public OutputUnitKey(String evaluationPath, String schemaLocation, String instanceLocation) { + super(); + this.evaluationPath = evaluationPath; + this.schemaLocation = schemaLocation; + this.instanceLocation = instanceLocation; + } + + public String getEvaluationPath() { + return evaluationPath; + } + + public String getSchemaLocation() { + return schemaLocation; + } + + public String getInstanceLocation() { + return instanceLocation; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, schemaLocation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputUnitKey other = (OutputUnitKey) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation); + } +} \ No newline at end of file From 96d3619369c67bc9dd1d2b8d27f0faa06dd85f7c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:35:13 +0800 Subject: [PATCH 33/53] Collect format annotations --- src/main/java/com/networknt/schema/FormatValidator.java | 7 +++++++ .../com/networknt/schema/format/DateTimeValidator.java | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index eaa057897..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(); diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 1ba046e9a..0e61f258f 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -47,10 +47,16 @@ public DateTimeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); + if (collectAnnotations(executionContext, "format")) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).keyword("format").value(DATETIME)); + } + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { return Collections.emptySet(); } + boolean assertionsEnabled = isAssertionsEnabled(executionContext); if (!isLegalDateTime(node.textValue())) { From 752bb86b94cdb3572a6e0ec74850458514a4be8d Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 20:53:44 +0800 Subject: [PATCH 34/53] Refactor --- .../HierarchicalOutputUnitFormatter.java | 38 ++++++++++++++++++ .../output/ListOutputUnitFormatter.java | 6 +-- .../schema/output/OutputUnitData.java | 20 +++++++--- .../schema/output/OutputUnitKey.java | 17 ++++---- .../schema/output/OutputUnitDataTest.java | 39 +++++++++++++++++++ 5 files changed, 104 insertions(+), 16 deletions(-) create mode 100644 src/test/java/com/networknt/schema/output/OutputUnitDataTest.java diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java index 5e23b8725..3d0cc851b 100644 --- a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java @@ -15,9 +15,15 @@ */ package com.networknt.schema.output; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Set; +import java.util.Stack; import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -27,6 +33,38 @@ public class HierarchicalOutputUnitFormatter { public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) { + + OutputUnit root = new OutputUnit(); + root.setValid(validationMessages.isEmpty()); + + root.setInstanceLocation(validationContext.getConfig().getPathType().getRoot()); + root.setEvaluationPath(validationContext.getConfig().getPathType().getRoot()); + // Determine the root schema later + + OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); + + Map valid = data.getValid(); + Map> errors = data.getErrors(); + Map> annotations = data.getAnnotations(); + Map> droppedAnnotations = data.getDroppedAnnotations(); + + // Evaluation path to output unit + Map index = new LinkedHashMap<>(); + index.put(new JsonNodePath(validationContext.getConfig().getPathType()), root); + return null; } + + protected static void process(OutputUnitKey key, Map index) { + if(index.containsKey(key.getEvaluationPath())) { + return; + } + // Ensure the path is created + JsonNodePath path = key.getEvaluationPath(); + Deque stack = new ArrayDeque<>(); + while(!index.containsKey(path)) { + stack.push(path); + } + + } } diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java index a7d2ff21d..009048588 100644 --- a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java @@ -47,9 +47,9 @@ public static OutputUnit format(Set validationMessages, Execu OutputUnit output = new OutputUnit(); OutputUnitKey key = entry.getKey(); output.setValid(entry.getValue()); - output.setEvaluationPath(key.getEvaluationPath()); - output.setSchemaLocation(key.getSchemaLocation()); - output.setInstanceLocation(key.getInstanceLocation()); + output.setEvaluationPath(key.getEvaluationPath().toString()); + output.setSchemaLocation(key.getSchemaLocation().toString()); + output.setInstanceLocation(key.getInstanceLocation().toString()); // Errors Map errorMap = errors.get(key); diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java index cffd15087..3f46a844e 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnitData.java +++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java @@ -52,9 +52,17 @@ public Map> getDroppedAnnotations() { } public static String formatMessage(String message) { - int index = message.indexOf(": "); + int index = message.indexOf(":"); if (index != -1) { - return message.substring(index + 2); + int length = message.length(); + while (index + 1 < length) { + if (message.charAt(index + 1) == ' ') { + index++; + } else { + break; + } + } + return message.substring(index + 1); } return message; } @@ -70,8 +78,8 @@ public static OutputUnitData from(Set validationMessages, Exe for (ValidationMessage assertion : validationMessages) { SchemaLocation assertionSchemaLocation = new SchemaLocation(assertion.getSchemaLocation().getAbsoluteIri(), assertion.getSchemaLocation().getFragment().getParent()); - OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent().toString(), - assertionSchemaLocation.toString(), assertion.getInstanceLocation().toString()); + OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent(), + assertionSchemaLocation, assertion.getInstanceLocation()); valid.put(key, false); Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); @@ -87,8 +95,8 @@ public static OutputUnitData from(Set validationMessages, Exe annotation.getSchemaLocation().getAbsoluteIri(), annotation.getSchemaLocation().getFragment().getParent()); - OutputUnitKey key = new OutputUnitKey(annotation.getEvaluationPath().getParent().toString(), - annotationSchemaLocation.toString(), annotation.getInstanceLocation().toString()); + OutputUnitKey key = new OutputUnitKey(annotation.getEvaluationPath().getParent(), + annotationSchemaLocation, annotation.getInstanceLocation()); boolean validResult = executionContext.getResults().isValid(annotation.getInstanceLocation(), annotation.getEvaluationPath()); valid.put(key, validResult); diff --git a/src/main/java/com/networknt/schema/output/OutputUnitKey.java b/src/main/java/com/networknt/schema/output/OutputUnitKey.java index 3c1a0ab09..5c99151af 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnitKey.java +++ b/src/main/java/com/networknt/schema/output/OutputUnitKey.java @@ -17,30 +17,33 @@ import java.util.Objects; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + /** * Output Unit Key. */ public class OutputUnitKey { - final String evaluationPath; - final String schemaLocation; - final String instanceLocation; + final JsonNodePath evaluationPath; + final SchemaLocation schemaLocation; + final JsonNodePath instanceLocation; - public OutputUnitKey(String evaluationPath, String schemaLocation, String instanceLocation) { + public OutputUnitKey(JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNodePath instanceLocation) { super(); this.evaluationPath = evaluationPath; this.schemaLocation = schemaLocation; this.instanceLocation = instanceLocation; } - public String getEvaluationPath() { + public JsonNodePath getEvaluationPath() { return evaluationPath; } - public String getSchemaLocation() { + public SchemaLocation getSchemaLocation() { return schemaLocation; } - public String getInstanceLocation() { + public JsonNodePath getInstanceLocation() { return instanceLocation; } diff --git a/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java b/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java new file mode 100644 index 000000000..098ba11ae --- /dev/null +++ b/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java @@ -0,0 +1,39 @@ +/* + * 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.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * OutputUnitDataTest. + */ +class OutputUnitDataTest { + + @Test + void format() { + String result = OutputUnitData.formatMessage("hello:"); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: "); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: "); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: world"); + assertEquals("world", result); + } +} From 688bc6c76554fec5c308681d351771714b5a14aa Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 23:51:33 +0800 Subject: [PATCH 35/53] Refactor --- .../networknt/schema/BaseJsonValidator.java | 33 ---- .../java/com/networknt/schema/JsonSchema.java | 38 +++++ .../com/networknt/schema/OutputFormat.java | 52 +++---- .../networknt/schema/output/OutputFlag.java | 47 ++++++ .../networknt/schema/output/OutputUnit.java | 38 ++++- .../schema/Issue366FailFastTest.java | 147 +++++++++--------- .../java/com/networknt/schema/Issue792.java | 6 +- .../networknt/schema/V4JsonSchemaTest.java | 38 ++--- 8 files changed, 228 insertions(+), 171 deletions(-) create mode 100644 src/main/java/com/networknt/schema/output/OutputFlag.java diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 659318d75..8419ea3b3 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -287,39 +287,6 @@ public Set validate(ExecutionContext executionContext, JsonNo return validate(executionContext, node, node, atRoot()); } - /** - * Validates to a format. - * - * @param the result type - * @param executionContext the execution context - * @param node the node - * @param format the format - * @return the result - */ - public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format) { - return validate(executionContext, node, format, null); - } - - /** - * Validates to a format. - * - * @param the result type - * @param executionContext the execution context - * @param node the node - * @param format the format - * @param executionCustomizer the customizer - * @return the result - */ - public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format, - ExecutionContextCustomizer executionCustomizer) { - format.customize(executionContext, this.validationContext); - if (executionCustomizer != null) { - executionCustomizer.customize(executionContext, validationContext); - } - Set validationMessages = validate(executionContext, node); - return format.format(validationMessages, executionContext, this.validationContext); - } - protected String getNodeFieldType() { JsonNode typeField = this.getParentSchema().getSchemaNode().get("type"); if (typeField != null) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 66f795346..a11c2ae9c 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -832,6 +832,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, 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. * diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java index a1aa9b24b..b499bc7be 100644 --- a/src/main/java/com/networknt/schema/OutputFormat.java +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -19,6 +19,7 @@ import com.networknt.schema.output.HierarchicalOutputUnitFormatter; import com.networknt.schema.output.ListOutputUnitFormatter; +import com.networknt.schema.output.OutputFlag; import com.networknt.schema.output.OutputUnit; /** @@ -41,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. @@ -85,8 +88,8 @@ public void customize(ExecutionContext executionContext, ValidationContext valid } @Override - public Set format(Set validationMessages, - ExecutionContext executionContext, ValidationContext validationContext) { + public Set format(JsonSchema jsonSchema, + Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) { return validationMessages; } } @@ -94,16 +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().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()); } } @@ -114,11 +118,12 @@ public static class Boolean implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { 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(); } } @@ -132,8 +137,8 @@ public void customize(ExecutionContext executionContext, ValidationContext valid } @Override - public OutputUnit format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext) { + public OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext); } } @@ -147,24 +152,9 @@ public void customize(ExecutionContext executionContext, ValidationContext valid } @Override - public OutputUnit format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext) { + public OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { return HierarchicalOutputUnitFormatter.format(validationMessages, executionContext, validationContext); } } - - /** - * The Flag output results. - */ - public static class FlagOutput { - private final boolean valid; - - public FlagOutput(boolean valid) { - this.valid = valid; - } - - public boolean isValid() { - return this.valid; - } - } } diff --git a/src/main/java/com/networknt/schema/output/OutputFlag.java b/src/main/java/com/networknt/schema/output/OutputFlag.java new file mode 100644 index 000000000..413bce06d --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputFlag.java @@ -0,0 +1,47 @@ +package com.networknt.schema.output; + +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * The Flag output results. + */ +public class OutputFlag { + private final boolean valid; + + public OutputFlag(boolean valid) { + this.valid = valid; + } + + public boolean isValid() { + return this.valid; + } + + @Override + public int hashCode() { + return Objects.hash(valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputFlag other = (OutputFlag) obj; + return valid == other.valid; + } + + @Override + public String toString() { + try { + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + return "OutputFlag [valid=" + valid + "]"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/output/OutputUnit.java b/src/main/java/com/networknt/schema/output/OutputUnit.java index 1e12ba87c..7f0dac946 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnit.java +++ b/src/main/java/com/networknt/schema/output/OutputUnit.java @@ -17,10 +17,13 @@ import java.util.List; import java.util.Map; +import java.util.Objects; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.networknt.schema.serialization.JsonMapperFactory; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonProcessingException; /** * Represents an output unit. @@ -111,5 +114,38 @@ public List getDetails() { public void setDetails(List details) { this.details = details; } - + + @Override + public int hashCode() { + return Objects.hash(annotations, details, droppedAnnotations, errors, evaluationPath, instanceLocation, + schemaLocation, valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputUnit other = (OutputUnit) obj; + return Objects.equals(annotations, other.annotations) && Objects.equals(details, other.details) + && Objects.equals(droppedAnnotations, other.droppedAnnotations) && Objects.equals(errors, other.errors) + && Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation) && valid == other.valid; + } + + @Override + public String toString() { + try { + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + return "OutputUnit [valid=" + valid + ", evaluationPath=" + evaluationPath + ", schemaLocation=" + + schemaLocation + ", instanceLocation=" + instanceLocation + ", errors=" + errors + + ", annotations=" + annotations + ", droppedAnnotations=" + droppedAnnotations + ", details=" + + details + "]"; + } + } } diff --git a/src/test/java/com/networknt/schema/Issue366FailFastTest.java b/src/test/java/com/networknt/schema/Issue366FailFastTest.java index eab951fc6..55c94abbf 100644 --- a/src/test/java/com/networknt/schema/Issue366FailFastTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailFastTest.java @@ -1,7 +1,7 @@ package com.networknt.schema; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -14,92 +14,87 @@ public class Issue366FailFastTest { - @BeforeEach - public void setup() throws IOException { - setupSchema(); - } - - JsonSchema jsonSchema; - ObjectMapper objectMapper = new ObjectMapper(); - private void setupSchema() throws IOException { - - SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); - schemaValidatorsConfig.setFailFast(true); - JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .jsonMapper(objectMapper) - .build(); - - schemaValidatorsConfig.setTypeLoose(false); - - SchemaLocation uri = getSchema(); - - InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); - JsonNode testCases = objectMapper.readValue(in, JsonNode.class); - this.jsonSchema = schemaFactory.getSchema(uri, testCases,schemaValidatorsConfig); - } - - protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(content); - return node; - } - - @Test - public void firstOneValid() throws Exception { - String dataPath = "/data/issue366.json"; - - InputStream dataInputStream = getClass().getResourceAsStream(dataPath); - JsonNode node = getJsonNodeFromStreamContent(dataInputStream); - List testNodes = node.findValues("tests"); - JsonNode testNode = testNodes.get(0).get(0); - JsonNode dataNode = testNode.get("data"); - Set errors = jsonSchema.validate(dataNode); - assertTrue(errors.isEmpty()); - } - - @Test - public void secondOneValid() throws Exception { - String dataPath = "/data/issue366.json"; - - InputStream dataInputStream = getClass().getResourceAsStream(dataPath); - JsonNode node = getJsonNodeFromStreamContent(dataInputStream); - List testNodes = node.findValues("tests"); - JsonNode testNode = testNodes.get(0).get(1); - JsonNode dataNode = testNode.get("data"); - Set errors = jsonSchema.validate(dataNode); - assertTrue(errors.isEmpty()); - } - - @Test - public void bothValid() throws Exception { - String dataPath = "/data/issue366.json"; - - assertThrows(JsonSchemaException.class, () -> { + @BeforeEach + public void setup() throws IOException { + setupSchema(); + } + + JsonSchema jsonSchema; + ObjectMapper objectMapper = new ObjectMapper(); + + private void setupSchema() throws IOException { + + SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); + schemaValidatorsConfig.setFailFast(true); + JsonSchemaFactory schemaFactory = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).jsonMapper(objectMapper).build(); + + schemaValidatorsConfig.setTypeLoose(false); + + SchemaLocation uri = getSchema(); + + InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); + JsonNode testCases = objectMapper.readValue(in, JsonNode.class); + this.jsonSchema = schemaFactory.getSchema(uri, testCases, schemaValidatorsConfig); + } + + protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(content); + return node; + } + + @Test + public void firstOneValid() throws Exception { + String dataPath = "/data/issue366.json"; + + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); + JsonNode node = getJsonNodeFromStreamContent(dataInputStream); + List testNodes = node.findValues("tests"); + JsonNode testNode = testNodes.get(0).get(0); + JsonNode dataNode = testNode.get("data"); + Set errors = jsonSchema.validate(dataNode); + assertTrue(errors.isEmpty()); + } + + @Test + public void secondOneValid() throws Exception { + String dataPath = "/data/issue366.json"; + + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); + JsonNode node = getJsonNodeFromStreamContent(dataInputStream); + List testNodes = node.findValues("tests"); + JsonNode testNode = testNodes.get(0).get(1); + JsonNode dataNode = testNode.get("data"); + Set errors = jsonSchema.validate(dataNode); + assertTrue(errors.isEmpty()); + } + + @Test + public void bothValid() throws Exception { + String dataPath = "/data/issue366.json"; + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); JsonNode node = getJsonNodeFromStreamContent(dataInputStream); List testNodes = node.findValues("tests"); JsonNode testNode = testNodes.get(0).get(2); JsonNode dataNode = testNode.get("data"); - jsonSchema.validate(dataNode); - }); - } + assertEquals(1, jsonSchema.validate(dataNode).size()); + } - @Test - public void neitherValid() throws Exception { - String dataPath = "/data/issue366.json"; + @Test + public void neitherValid() throws Exception { + String dataPath = "/data/issue366.json"; - assertThrows(JsonSchemaException.class, () -> { InputStream dataInputStream = getClass().getResourceAsStream(dataPath); JsonNode node = getJsonNodeFromStreamContent(dataInputStream); List testNodes = node.findValues("tests"); JsonNode testNode = testNodes.get(0).get(3); JsonNode dataNode = testNode.get("data"); - jsonSchema.validate(dataNode); - }); - } + assertEquals(1, jsonSchema.validate(dataNode).size()); + } - private SchemaLocation getSchema() { - return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); - } + private SchemaLocation getSchema() { + return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); + } } diff --git a/src/test/java/com/networknt/schema/Issue792.java b/src/test/java/com/networknt/schema/Issue792.java index 2ca3f238e..935336e6b 100644 --- a/src/test/java/com/networknt/schema/Issue792.java +++ b/src/test/java/com/networknt/schema/Issue792.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import static org.junit.jupiter.api.Assertions.assertThrows; + +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ void test() throws JsonProcessingException { JsonSchema jsonSchema = schemaFactory.getSchema(schemaDef, config); JsonNode jsonNode = new ObjectMapper().readTree("{\"field\": \"pattern-violation\"}"); - //this works with 1.0.81, but not with 1.0.82+ - assertThrows(JsonSchemaException.class, () -> jsonSchema.validate(jsonNode)); + assertEquals(1, jsonSchema.validate(jsonNode).size()); } } diff --git a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java index 59397702a..1fc010276 100644 --- a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java @@ -45,13 +45,9 @@ public void testLoadingWithId() throws Exception { */ @Test public void testFailFast_AllErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-all-errors-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-all-errors-data.json"); + assertEquals(1, messages.size()); } /** @@ -59,13 +55,9 @@ public void testFailFast_AllErrors() throws IOException { */ @Test public void testFailFast_OneErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-one-error-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-one-error-data.json"); + assertEquals(1, messages.size()); } /** @@ -73,13 +65,9 @@ public void testFailFast_OneErrors() throws IOException { */ @Test public void testFailFast_TwoErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-two-errors-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-two-errors-data.json"); + assertEquals(1, messages.size()); } /** @@ -88,13 +76,9 @@ public void testFailFast_TwoErrors() throws IOException { */ @Test public void testFailFast_NoErrors() throws IOException { - try { - final Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + final Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-no-errors-data.json"); - assertTrue(messages.isEmpty()); - } catch (JsonSchemaException e) { - fail("Must not get an errors"); - } + assertTrue(messages.isEmpty()); } private Set validateFailingFastSchemaFor(final String schemaFileName, final String dataFileName) throws IOException { From e1492e6d0ee55e6f5d48faea990be8cd32dfc159 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 30 Jan 2024 23:53:21 +0800 Subject: [PATCH 36/53] Refactor --- src/main/java/com/networknt/schema/OutputFormat.java | 2 +- .../schema/output/HierarchicalOutputUnitFormatter.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java index b499bc7be..b3c56e7ad 100644 --- a/src/main/java/com/networknt/schema/OutputFormat.java +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -154,7 +154,7 @@ public void customize(ExecutionContext executionContext, ValidationContext valid @Override public OutputUnit format(JsonSchema jsonSchema, Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) { - return HierarchicalOutputUnitFormatter.format(validationMessages, executionContext, validationContext); + return HierarchicalOutputUnitFormatter.format(jsonSchema, validationMessages, executionContext, validationContext); } } } diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java index 3d0cc851b..75b28cde9 100644 --- a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java @@ -24,6 +24,7 @@ import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; +import com.networknt.schema.JsonSchema; import com.networknt.schema.ValidationContext; import com.networknt.schema.ValidationMessage; @@ -31,15 +32,15 @@ * HierarchicalOutputUnitFormatter. */ public class HierarchicalOutputUnitFormatter { - public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext) { + public static OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { OutputUnit root = new OutputUnit(); root.setValid(validationMessages.isEmpty()); root.setInstanceLocation(validationContext.getConfig().getPathType().getRoot()); root.setEvaluationPath(validationContext.getConfig().getPathType().getRoot()); - // Determine the root schema later + root.setSchemaLocation(jsonSchema.getSchemaLocation().toString()); OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); From e4bdde9f55eeb3556b8f0d7574ca8b9b5238620b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 01:15:21 +0800 Subject: [PATCH 37/53] Refactor --- .../schema/FailFastAssertionException.java | 36 +++++++++++++++++-- .../java/com/networknt/schema/JsonSchema.java | 2 +- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/FailFastAssertionException.java b/src/main/java/com/networknt/schema/FailFastAssertionException.java index 45dd3804e..6331add69 100644 --- a/src/main/java/com/networknt/schema/FailFastAssertionException.java +++ b/src/main/java/com/networknt/schema/FailFastAssertionException.java @@ -16,15 +16,47 @@ 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 JsonSchemaException { +public class FailFastAssertionException extends RuntimeException { private static final long serialVersionUID = 1L; + private final ValidationMessage validationMessage; + public FailFastAssertionException(ValidationMessage validationMessage) { - super(Objects.requireNonNull(validationMessage)); + this.validationMessage = Objects.requireNonNull(validationMessage); + } + + public ValidationMessage getValidationMessage() { + return this.validationMessage; + } + + 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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index a11c2ae9c..abf532b7c 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -859,7 +859,7 @@ public T validate(ExecutionContext executionContext, JsonNode node, OutputFo ExecutionContextCustomizer executionCustomizer) { format.customize(executionContext, this.validationContext); if (executionCustomizer != null) { - executionCustomizer.customize(executionContext, validationContext); + executionCustomizer.customize(executionContext, this.validationContext); } Set validationMessages = null; try { From 8c9bea12be486330fc255446c6da5ffb42680641 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 01:42:21 +0800 Subject: [PATCH 38/53] Redesign and fix fail fast logic --- .../com/networknt/schema/AnyOfValidator.java | 28 ++++-- .../networknt/schema/ContainsValidator.java | 20 ++-- .../com/networknt/schema/IfValidator.java | 26 +++-- .../com/networknt/schema/NotValidator.java | 23 +++-- .../com/networknt/schema/OneOfValidator.java | 99 ++++++++++--------- .../networknt/schema/UnionTypeValidator.java | 18 +++- .../schema/ValidationMessageHandler.java | 78 +-------------- 7 files changed, 128 insertions(+), 164 deletions(-) diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 179e1c480..c21519052 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -65,18 +65,21 @@ public Set validate(ExecutionContext executionContext, JsonNo int numberOfValidSubSchemas = 0; try { - for (JsonSchema schema: this.schemas) { - Set errors = Collections.emptySet(); - try { + // 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 errors = Collections.emptySet(); state.setMatchedNode(initialHasMatchedNode); TypeValidator typeValidator = schema.getTypeValidator(); if (typeValidator != null) { - //If schema has type validator and node type doesn't match with schemaType then ignore it - //For union type, it is a must to call TypeValidator + // If schema has type validator and node type doesn't match with schemaType then + // ignore it + // For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors - .addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); + allErrors.addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); continue; } } @@ -88,7 +91,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // check if any validation errors have occurred if (errors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator + // check whether there are no errors HOWEVER we have validated the exact + // validator if (!state.hasMatchedNode()) { continue; } @@ -96,7 +100,8 @@ public Set validate(ExecutionContext executionContext, JsonNo numberOfValidSubSchemas++; } - if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) && canShortCircuit()) { + if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) + && canShortCircuit()) { // Clear all errors. allErrors.clear(); // return empty errors. @@ -117,8 +122,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } } allErrors.addAll(errors); - } finally { } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } // determine only those errors which are NOT of type "required" property missing @@ -182,5 +189,6 @@ protected boolean canShortCircuit() { @Override public void preloadJsonSchema() { preloadJsonSchemas(this.schemas); + canShortCircuit(); // cache flag } } \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 64806d59d..0bec642d5 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -76,13 +76,21 @@ public Set validate(ExecutionContext executionContext, JsonNo int actual = 0, i = 0; List indexes = new ArrayList<>(); // for the annotation if (null != this.schema && node.isArray()) { - for (JsonNode n : node) { - JsonNodePath path = instanceLocation.append(i); - if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { - ++actual; - indexes.add(i); + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + for (JsonNode n : node) { + JsonNodePath path = instanceLocation.append(i); + if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { + ++actual; + indexes.add(i); + } + ++i; } - ++i; + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } int m = 1; // default to 1 if "min" not specified if (this.min != null) { diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 42bfdb99d..77d387e3f 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -66,24 +66,22 @@ public Set validate(ExecutionContext executionContext, JsonNo Set errors = new LinkedHashSet<>(); 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) { - 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 { + // 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/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 310122bf0..e8d88edf6 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -35,20 +35,25 @@ public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - Set errors = new HashSet<>(); + Set errors = null; + debug(logger, node, rootNode, instanceLocation); + // 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().instanceNode(node).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString()) - .build()); - } - return Collections.emptySet(); } finally { + // 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 diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 12ebb0766..5638759bc 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -43,69 +43,75 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { Set errors = new LinkedHashSet<>(); - 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(); - 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; + // Reset state in case the previous validator did not match + state.setMatchedNode(true); - numberOfValidSchema++; - } + if (!state.isWalkEnabled()) { + schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + } else { + schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation, + state.isValidationEnabled()); + } - if (numberOfValidSchema > 1 && canShortCircuit()) { - // short-circuit - break; + // 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++; + } - childErrors.addAll(schemaErrors); - } finally { + if (numberOfValidSchema > 1 && canShortCircuit()) { + // short-circuit + break; } - } - // 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); + childErrors.addAll(schemaErrors); } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); + } - // Make sure to signal parent handlers we matched - if (errors.isEmpty()) - state.setMatchedNode(true); - - // reset the ValidatorState object - resetValidatorState(executionContext); + // 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); + } - return Collections.unmodifiableSet(errors); - } finally { + // Make sure to signal parent handlers we matched + if (errors.isEmpty()) { + state.setMatchedNode(true); } + + // reset the ValidatorState object + resetValidatorState(executionContext); + + return Collections.unmodifiableSet(errors); } protected boolean canShortCircuit() { @@ -146,5 +152,6 @@ public void preloadJsonSchema() { for (JsonSchema schema: this.schemas) { schema.initializeValidators(); } + canShortCircuit(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index ac5294c90..3b5e898b5 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -69,12 +69,20 @@ public Set validate(ExecutionContext executionContext, JsonNo boolean valid = false; - for (JsonValidator schema : schemas) { - Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); - if (errors == null || errors.isEmpty()) { - valid = true; - break; + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + for (JsonValidator schema : schemas) { + Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); + if (errors == null || errors.isEmpty()) { + valid = true; + break; + } } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } if (!valid) { diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index cde6904ff..3804e4aba 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -5,9 +5,11 @@ import com.networknt.schema.utils.StringUtils; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; +import java.util.Set; public abstract class ValidationMessageHandler { protected final MessageSource messageSource; @@ -55,7 +57,7 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { protected MessageSourceValidationMessage.Builder message() { return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, (message, failFast) -> { - if (failFast && canFailFast()) { + if (failFast) { throw new FailFastAssertionException(message); } }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) @@ -67,78 +69,6 @@ protected ErrorMessageType getErrorMessageType() { return this.errorMessageType; } - /** - * Determines if the evaluation can fast fail. - * - * @return true if it can fast fail - */ - protected boolean canFailFast() { - return !hasApplicatorInEvaluationPath(); - } - - /** - * Determines if there is an applicator in the evaluation path for determining - * if it is possible to fast fail. - *

- * For instance if there is a not keyword in the evaluation path this can change - * the overall result. - * - * @return true if there is an applicator in the evaluation path - */ - private boolean hasApplicatorInEvaluationPath() { - return hasAnyOfInEvaluationPath() || hasIfInEvaluationPath() || hasNotInEvaluationPath() - || hasOneOfInEvaluationPath(); - } - - /** - * Determines if anyOf is in the evaluation path. - * - * @return true if anyOf is in the evaluation path - */ - private boolean hasAnyOfInEvaluationPath() { - return hasKeywordInEvaluationPath(ValidatorTypeCode.ANY_OF.getValue()); - } - - /** - * Determines if if is in the evaluation path. - * - * @return true if if is in the evaluation path - */ - private boolean hasIfInEvaluationPath() { - return hasKeywordInEvaluationPath(ValidatorTypeCode.IF_THEN_ELSE.getValue()); - } - - /** - * Determines if not is in the evaluation path. - * - * @return true if not is in the evaluation path - */ - private boolean hasNotInEvaluationPath() { - return hasKeywordInEvaluationPath(ValidatorTypeCode.NOT.getValue()); - } - - /** - * Determines if oneOf is in the evaluation path. - * - * @return true if oneOf is in the evaluation path - */ - protected boolean hasOneOfInEvaluationPath() { - return hasKeywordInEvaluationPath(ValidatorTypeCode.ONE_OF.getValue()); - } - - /** - * Determines if keyword is in the evaluation path. - * - * @param keyword the keyword - * @return true if the keyword is in the evaluation path - */ - private boolean hasKeywordInEvaluationPath(String keyword) { - // Parent is used as if the current path is an applicator this should still - // throw if there is no other applicators in the rest of the evaluation path - JsonNodePath parent = this.evaluationPath.getParent(); - return parent != null ? parent.contains(keyword) : false; - } - protected void parseErrorCode(String errorCodeKey) { if (errorCodeKey != null && this.parentSchema != null) { JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey); @@ -213,7 +143,7 @@ protected JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, } return messageNode; } - + protected String getErrorCodeKey(String keyword) { if (keyword != null) { return keyword + "ErrorCode"; From 5db1d4ad71be80d325b290e87353611f3f6a9e21 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 11:19:59 +0800 Subject: [PATCH 39/53] Support hierarchical output --- .../HierarchicalOutputUnitFormatter.java | 98 +++++++++++- .../schema/output/OutputUnitData.java | 7 +- .../com/networknt/schema/OutputUnitTest.java | 143 ++++++++++++++++++ 3 files changed, 239 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java index 75b28cde9..51950baec 100644 --- a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java @@ -16,11 +16,13 @@ package com.networknt.schema.output; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; -import java.util.Stack; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; @@ -53,19 +55,105 @@ public static OutputUnit format(JsonSchema jsonSchema, Set va Map index = new LinkedHashMap<>(); index.put(new JsonNodePath(validationContext.getConfig().getPathType()), root); - return null; + // Get all the evaluation paths with data + Set keys = new LinkedHashSet<>(); + errors.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + annotations.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + droppedAnnotations.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + + errors.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + annotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + droppedAnnotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + + // Process all the data + for (Entry> error : errors.entrySet()) { + OutputUnitKey key = error.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + unit.setInstanceLocation(key.getInstanceLocation().toString()); + unit.setSchemaLocation(key.getSchemaLocation().toString()); + unit.setValid(false); + unit.setErrors(error.getValue()); + } + + for (Entry> annotation : annotations.entrySet()) { + OutputUnitKey key = annotation.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + String instanceLocation = key.getInstanceLocation().toString(); + String schemaLocation = key.getSchemaLocation().toString(); + if (unit.getInstanceLocation() != null && !unit.getInstanceLocation().equals(instanceLocation)) { + throw new IllegalArgumentException(); + } + if (unit.getSchemaLocation() != null && !unit.getSchemaLocation().equals(schemaLocation)) { + throw new IllegalArgumentException(); + } + unit.setInstanceLocation(instanceLocation); + unit.setSchemaLocation(schemaLocation); + unit.setAnnotations(annotation.getValue()); + unit.setValid(valid.get(key)); + } + + for (Entry> droppedAnnotation : droppedAnnotations.entrySet()) { + OutputUnitKey key = droppedAnnotation.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + String instanceLocation = key.getInstanceLocation().toString(); + String schemaLocation = key.getSchemaLocation().toString(); + if (unit.getInstanceLocation() != null && !unit.getInstanceLocation().equals(instanceLocation)) { + throw new IllegalArgumentException(); + } + if (unit.getSchemaLocation() != null && !unit.getSchemaLocation().equals(schemaLocation)) { + throw new IllegalArgumentException(); + } + unit.setInstanceLocation(instanceLocation); + unit.setSchemaLocation(schemaLocation); + unit.setDroppedAnnotations(droppedAnnotation.getValue()); + unit.setValid(valid.get(key)); + } + return root; } - protected static void process(OutputUnitKey key, Map index) { - if(index.containsKey(key.getEvaluationPath())) { + /** + * Builds in the index of evaluation path to output units to be populated later + * and modify the root to add the appropriate children. + * + * @param key the current key to process + * @param index contains all the mappings from evaluation path to output units + * @param keys that contain all the evaluation paths with data + * @param root the root output unit + */ + protected static void buildIndex(OutputUnitKey key, Map index, Set keys, + OutputUnit root) { + if (index.containsKey(key.getEvaluationPath())) { return; } // Ensure the path is created JsonNodePath path = key.getEvaluationPath(); Deque stack = new ArrayDeque<>(); - while(!index.containsKey(path)) { + while (path != null && path.getElement(-1) != null) { stack.push(path); + path = path.getParent(); } + OutputUnit parent = root; + while (!stack.isEmpty()) { + JsonNodePath current = stack.pop(); + if (!index.containsKey(current) && keys.contains(current)) { + // the index doesn't contain this path but this is a path with data + OutputUnit child = new OutputUnit(); + child.setValid(true); + child.setEvaluationPath(current.toString()); + index.put(current, child); + if (parent.getDetails() == null) { + parent.setDetails(new ArrayList<>()); + } + parent.getDetails().add(child); + } + + // If exists in the index this is the new parent + // Otherwise this is an evaluation path with no data and hence should be skipped + OutputUnit child = index.get(current); + if (child != null) { + parent = child; + } + } } } diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java index 3f46a844e..b6ab108ce 100644 --- a/src/main/java/com/networknt/schema/output/OutputUnitData.java +++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java @@ -15,7 +15,6 @@ */ package com.networknt.schema.output; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -31,9 +30,9 @@ */ public class OutputUnitData { private final Map valid = new LinkedHashMap<>(); - private final Map> errors = new HashMap<>(); - private final Map> annotations = new HashMap<>(); - private final Map> droppedAnnotations = new HashMap<>(); + private final Map> errors = new LinkedHashMap<>(); + private final Map> annotations = new LinkedHashMap<>(); + private final Map> droppedAnnotations = new LinkedHashMap<>(); public Map getValid() { return valid; diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java index 0d5587ae0..f4a117c7c 100644 --- a/src/test/java/com/networknt/schema/OutputUnitTest.java +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -161,6 +161,149 @@ void annotationCollectionList() throws JsonProcessingException { + " } ]\r\n" + "}"; assertEquals(expected, output); + } + + @Test + void annotationCollectionHierarchical() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData1; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); + String expected = "{\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" + + " \"instanceLocation\" : \"\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" + + " \"title\" : \"root\"\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/0\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\r\n" + + " \"instanceLocation\" : \"/foo\",\r\n" + + " \"errors\" : {\r\n" + + " \"required\" : \"required property 'unspecified-prop' not found\"\r\n" + + " }\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" + + " \"instanceLocation\" : \"/foo\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"foo-prop\" ],\r\n" + + " \"title\" : \"foo-title\",\r\n" + + " \"additionalProperties\" : [ \"foo-prop\", \"other-prop\" ]\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" + + " \"errors\" : {\r\n" + + " \"const\" : \"must be a constant value 1\"\r\n" + + " },\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"title\" : \"foo-prop-title\"\r\n" + + " }\r\n" + + " } ]\r\n" + + " }, {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" + + " \"instanceLocation\" : \"/bar\",\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"properties\" : [ \"bar-prop\" ],\r\n" + + " \"title\" : \"bar-title\"\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : false,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" + + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" + + " \"errors\" : {\r\n" + + " \"minimum\" : \"must have a minimum value of 10\"\r\n" + + " },\r\n" + + " \"droppedAnnotations\" : {\r\n" + + " \"title\" : \"bar-prop-title\"\r\n" + + " }\r\n" + + " } ]\r\n" + + " } ]\r\n" + + "}"; + assertEquals(expected, output); + } + + @Test + void annotationCollectionHierarchical2() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData2; + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); + String expected = "{\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" + + " \"instanceLocation\" : \"\",\r\n" + + " \"annotations\" : {\r\n" + + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" + + " \"title\" : \"root\"\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" + + " \"instanceLocation\" : \"/foo\",\r\n" + + " \"annotations\" : {\r\n" + + " \"properties\" : [ \"foo-prop\" ],\r\n" + + " \"title\" : \"foo-title\",\r\n" + + " \"additionalProperties\" : [ \"foo-prop\", \"unspecified-prop\" ]\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" + + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" + + " \"annotations\" : {\r\n" + + " \"title\" : \"foo-prop-title\"\r\n" + + " }\r\n" + + " } ]\r\n" + + " }, {\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" + + " \"instanceLocation\" : \"/bar\",\r\n" + + " \"annotations\" : {\r\n" + + " \"properties\" : [ \"bar-prop\" ],\r\n" + + " \"title\" : \"bar-title\"\r\n" + + " },\r\n" + + " \"details\" : [ {\r\n" + + " \"valid\" : true,\r\n" + + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" + + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" + + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" + + " \"annotations\" : {\r\n" + + " \"title\" : \"bar-prop-title\"\r\n" + + " }\r\n" + + " } ]\r\n" + + " } ]\r\n" + + "}"; + assertEquals(expected, output); } } From 482e52f4fe43902063ea2a4f00b833c51f48ae67 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:33:22 +0800 Subject: [PATCH 40/53] Update docs --- README.md | 184 ++++++++++++++++++++++++++++++++++++++++--- doc/compatibility.md | 13 +-- doc/upgrading.md | 12 ++- 3 files changed, 189 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index f778ee210..c355e9735 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ [![codecov.io](https://codecov.io/github/networknt/json-schema-validator/coverage.svg?branch=master)](https://codecov.io/github/networknt/json-schema-validator?branch=master) [![Javadocs](http://www.javadoc.io/badge/com.networknt/json-schema-validator.svg)](https://www.javadoc.io/doc/com.networknt/json-schema-validator) - This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. @@ -22,7 +21,7 @@ Information on the compatibility support for each version, including known issue ## Upgrading to new versions -This library can contain breaking changes in minor version releases. +This library can contain breaking changes in minor version releases that may require code changes. Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. @@ -32,8 +31,8 @@ Information on the latest version can be found on the [Releases](https://github. The [JSON Schema Validation Comparison ](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. -* [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional) -* [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance) +* [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional#summary-results-table) +* [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report](https://bowtie.report/) that compares functional characteristics of different implementations, including non-Java implementations, but does not do any performance benchmarking. @@ -43,11 +42,11 @@ The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report This should be the fastest Java JSON Schema Validator implementation. -The following is the benchmark results from [JSON Schema Validator Perftest](https://github.com/networknt/json-schema-validator-perftest) project that uses the [Java Microbenchmark Harness](https://github.com/openjdk/jmh). +The following is the benchmark results from the [JSON Schema Validator Perftest](https://github.com/networknt/json-schema-validator-perftest) project that uses the [Java Microbenchmark Harness](https://github.com/openjdk/jmh). -Note that the benchmark results are highly dependent on the input data workloads used for the validation. +Note that the benchmark results are highly dependent on the input data workloads and schemas used for the validation. -In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance) for benchmark results for more typical workloads +In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) for benchmark results for more typical workloads If performance is an important consideration, the specific sample workloads should be benchmarked, as there are different performance characteristics when certain keywords are used. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators, and annotation collection will adversely affect performance. @@ -81,6 +80,15 @@ EveritBenchmark.testValidate:·gc.count thrpt 10 EveritBenchmark.testValidate:·gc.time thrpt 10 148.000 ms ``` +#### Functionality + +This implementation is tested against the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). As tests are continually added to the suite, these test results may not be current. + +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| +| NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | + +* Note that this uses the ECMA 262 Validator option turned on for the `pattern` tests. #### Jackson Parser @@ -213,7 +221,9 @@ The following example demonstrates how inputs are validated against a schema. It * Using the schema to validate the data along with setting any execution specific configuration like for instance the locale or whether format assertions are enabled. ```java -// This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified in the schema data. If $schema is specified in the schema data then that schema dialect will be used instead and this version is ignored. +// This creates a schema factory that will use Draft 2012-12 as the default if $schema is not specified +// in the schema data. If $schema is specified in the schema data then that schema dialect will be used +// instead and this version is ignored. JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath:schema/ builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")) @@ -226,7 +236,8 @@ config.setPathType(PathType.JSON_POINTER); // Note that setting this to true requires including the optional joni dependency // config.setEcma262Validator(true); -// Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. +// Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. +// If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); String input = "{\r\n" + " \"main\": {\r\n" @@ -254,7 +265,9 @@ JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag. // This is better for performance and the remote may choose not to service the request // For instance Cloudflare will block requests that have older Java User-Agent strings eg. Java/1. builder.schemaMappers(schemaMappers -> - schemaMappers.mapPrefix("https://json-schema.org", "classpath:").mapPrefix("http://json-schema.org", "classpath:")) + schemaMappers + .mapPrefix("https://json-schema.org", "classpath:") + .mapPrefix("http://json-schema.org", "classpath:")) ); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); @@ -280,6 +293,153 @@ Set assertions = schema.validate(input, InputFormat.JSON, exe executionContext.getConfig().setFormatAssertionsEnabled(true); }); ``` +### Results and output formats + +#### Results + +The following types of results are generated by the library. + +| Type | Description +|-------------|------------------- +| Assertions | Validation errors generated by a keyword on a particular input data instance. This is generally described in a `ValidationMessage` or in a `OutputUnit`. Note that since Draft 2019-09 the `format` keyword no longer generates assertions by default and instead generates only annotations unless configured otherwise using a configuration option or by using a meta schema that uses the appropriate vocabulary. +| Annotations | Additional information generated by a keyword for a particular input data instance. This is generally described in a `OutputUnit`. Annotation collection and reporting is turned off by default. Annotations required by keywords such as `unevaluatedProperties` or `unevaluatedItems` are always collected for evaluation purposes and cannot be disabled but will not be reported unless configured to do so. + +The following information is used to describe both types of results. + +| Type | Description +|-------------------|------------------- +| Evaluation Path | This is the set of keys from the root through which evaluation passes to reach the schema for evaluating the instance. This includes `$ref` and `$dynamicRef`. eg. ```/properties/bar/$ref/properties/bar-prop``` +| Schema Location | This is the canonical IRI of the schema plus the JSON pointer fragment to the schema that was used for evaluating the instance. eg. ```https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop``` +| Instance Location | This is the JSON pointer fragment to the instance data that was being evaluated. eg. ```/bar/bar-prop``` + +Assertions contains the following additional information + +| Type | Description +|-------------------|------------------- +| Message | The validation error message. +| Code | The error code. +| Message Key | The message key used for generating the message for localization. +| Arguments | The arguments used for generating the message. +| Type | The keyword that generated the message. +| Property | The property name that caused the validation error for example for the `required` keyword. Note that this is not part of the instance location as that points to the instance node. +| Schema Node | The `JsonNode` pointed to by the Schema Location. +| Instance Node | The `JsonNode` pointed to by the Instance Location. +| Details | Additional details that can be set by custom keyword validator implementations. This is not used by the library. + +Annotations contains the following additional information + +| Type | Description +|-------------------|------------------- +| Value | The annotation value generated + + +#### Output formats + +This library implements the Flag, List and Hierarchical output formats defined in the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/8270653a9f59fadd2df0d789f22d486254505bbe/jsonschema-validation-output-machines.md). + +The List and Hierarchical output formats are particularly helpful for understanding how the system arrived at a particular result. + +| Output Format | Description +|-------------------|------------------- +| Default | Generates the list of assertions. +| Boolean | Returns `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. +| Flag | Returns an `OutputFlag` object with `valid` having `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. +| List | Returns an `OutputUnit` object with `details` with a list of `OutputUnit` objects with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. +| Hierarchical | Returns an `OutputUnit` object with a hierarchy of `OutputUnit` objects for the evaluation path with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. + +The following example shows how to generate the hierarchical output format with annotation collection and reporting turned on and format assertions turned on. + +```java +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setPathType(PathType.JSON_POINTER); +config.setFormatAssertionsEnabled(true); +JsonSchema schema = factory.getSchema(SchemaLocation.of("https://json-schema.org/schemas/example"), config); + +OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionContext.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); +}); +``` +The following is sample output from the Hierarchical format. + +```json +{ + "valid" : false, + "evaluationPath" : "", + "schemaLocation" : "https://json-schema.org/schemas/example#", + "instanceLocation" : "", + "droppedAnnotations" : { + "properties" : [ "foo", "bar" ], + "title" : "root" + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/0", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/0", + "instanceLocation" : "/foo", + "errors" : { + "required" : "required property 'unspecified-prop' not found" + } + }, { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/1", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1", + "instanceLocation" : "/foo", + "droppedAnnotations" : { + "properties" : [ "foo-prop" ], + "title" : "foo-title", + "additionalProperties" : [ "foo-prop", "other-prop" ] + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/1/properties/foo-prop", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop", + "instanceLocation" : "/foo/foo-prop", + "errors" : { + "const" : "must be a constant value 1" + }, + "droppedAnnotations" : { + "title" : "foo-prop-title" + } + } ] + }, { + "valid" : false, + "evaluationPath" : "/properties/bar/$ref", + "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar", + "instanceLocation" : "/bar", + "droppedAnnotations" : { + "properties" : [ "bar-prop" ], + "title" : "bar-title" + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/bar/$ref/properties/bar-prop", + "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop", + "instanceLocation" : "/bar/bar-prop", + "errors" : { + "minimum" : "must have a minimum value of 10" + }, + "droppedAnnotations" : { + "title" : "bar-prop-title" + } + } ] + } ] +} +``` + +## Configuration + +### Execution Configuration + +| Name | Description | Default Value +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- +| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` +| `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` +| `annotationCollectionPredicate`| The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` +| `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaValidatorsConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` + ## Performance Considerations @@ -298,6 +458,8 @@ This does not mean that using a schema with a later draft specification will aut ## [Quick Start](doc/quickstart.md) +## [Customizing Schema Retrieval](doc/schema-retrieval.md) + ## [Validators](doc/validators.md) ## [Configuration](doc/config.md) @@ -306,8 +468,6 @@ This does not mean that using a schema with a later draft specification will aut ## [YAML Validation](doc/yaml.md) -## [Customizing Schema Retrieval](doc/schema-retrieval.md) - ## [Customized MetaSchema](doc/cust-meta.md) ## [Collector Context](doc/collector-context.md) diff --git a/doc/compatibility.md b/doc/compatibility.md index 91c28f8e8..35f0bab4b 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -1,9 +1,11 @@ ## Compatibility with JSON Schema versions -This implementation does not currently generate annotations. - The `pattern` validator by default uses the JDK regular expression implementation which is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. The library can however be configured to use a ECMA-262 compliant regular expression implementation. +Annotation processing and reporting are implemented. Note that the collection of annotations will have an adverse performance impact. + +This implements the Flag, List and Hierarchical output formats defined in the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/8270653a9f59fadd2df0d789f22d486254505bbe/jsonschema-validation-output-machines.md). + ### Known Issues There are currently no known issues with the required functionality from the specification. @@ -14,7 +16,6 @@ The following are the tests results after running the [JSON Schema Test Suite](h |-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| | NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | - ### Legend | Symbol | Meaning | @@ -85,15 +86,15 @@ The following are the tests results after running the [JSON Schema Test Suite](h #### Content Encoding -Since Draft 2019-09, the `contentEncoding` keyword does not generate assertions. As the implementation currently does not collect annotations this only generates assertions in Draft 7. +Since Draft 2019-09, the `contentEncoding` keyword does not generate assertions. #### Content Media Type -Since Draft 2019-09, the `contentMediaType` keyword does not generate assertions. As the implementation currently does not collect annotations this only generates assertions in Draft 7. +Since Draft 2019-09, the `contentMediaType` keyword does not generate assertions. #### Content Schema -The `contentSchema` keyword does not generate assertions. As the implementation currently does not collect annotations this doesn't do anything. +The `contentSchema` keyword does not generate assertions. #### Pattern diff --git a/doc/upgrading.md b/doc/upgrading.md index 0004a680f..de02672fd 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -1,14 +1,22 @@ ## Upgrading to new versions +This library can contain breaking changes in minor version releases. + This contains information on the notable or breaking changes in each version. ### 1.3.1 This does not contain any breaking changes from 1.3.0 -This refactors the following keywords to improve performance and meet the functional requirements. +* Annotation collection and reporting has been implemented +* Keywords have been refactored to use annotations for evaluation to improve performance and meet functional requirements +* The list and hierarchical output formats have been implemented as per the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/main/jsonschema-validation-output-machines.md). +* The fail fast evaluation processing has been redesigned and fixed. This currently passes the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) with fail fast enabled. Previously contains and union type may cause incorrect results. +* This also contains fixes for regressions introduced in 1.3.0 + +The following keywords were refactored to improve performance and meet the functional requirements. -In particular this converts the `unevaluatedItems` and `unevaluatedProperties` validators to use annotations to perform the evaluation instead of the current mechanism which affects performance. This also refactors `$recursiveRef` not to rely on that same mechanism. +In particular this converts the `unevaluatedItems` and `unevaluatedProperties` validators to use annotations to perform the evaluation instead of the current mechanism which affects performance. This also refactors `$recursiveRef` to not rely on that same mechanism. * `unevaluatedProperties` * `unevaluatedItems` From 51641c94b64c6724868c8c809c44e12c5f6d8241 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 14:56:18 +0800 Subject: [PATCH 41/53] Throw specific exceptions if ref cannot be resolved --- .../networknt/schema/DynamicRefValidator.java | 62 +++++++---------- .../schema/InvalidSchemaRefException.java | 32 +++++++++ .../java/com/networknt/schema/JsonSchema.java | 11 ++- .../schema/RecursiveRefValidator.java | 67 ++++++++----------- .../com/networknt/schema/RefValidator.java | 62 ++++++++--------- 5 files changed, 119 insertions(+), 115 deletions(-) create mode 100644 src/main/java/com/networknt/schema/InvalidSchemaRefException.java diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index 4d4cfaf1d..a113d1167 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -23,7 +23,7 @@ import java.util.*; /** - * Resolves $dynamicRef. + * {@link JsonValidator} that resolves $dynamicRef. */ public class DynamicRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DynamicRefValidator.class); @@ -87,53 +87,39 @@ private static String resolve(JsonSchema parentSchema, String refValue) { @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - - Set errors = Collections.emptySet(); - - try { - debug(logger, node, rootNode, instanceLocation); - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().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 JsonSchemaException(validationMessage); - } - errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); - } finally { + 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) { - Set errors = Collections.emptySet(); - - 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.DYNAMIC_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 { + 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; @@ -151,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/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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index abf532b7c..b07f3c121 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -327,9 +327,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; } diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index a913e7bef..0ca0f26fb 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} that resolves $recursiveRef. + */ public class RecursiveRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class); @@ -32,12 +35,11 @@ public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluat 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()) .instanceNode(this.schemaNode) - .schemaNode(this.schemaNode) - .evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); + .evaluationPath(evaluationPath).arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); @@ -82,52 +84,39 @@ static JsonSchema getSchema(JsonSchema parentSchema, ValidationContext validatio @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - Set errors = Collections.emptySet(); - - try { - debug(logger, node, rootNode, instanceLocation); - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().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 JsonSchemaException(validationMessage); - } - errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); - } finally { + 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) { - Set errors = Collections.emptySet(); - - 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.RECURSIVE_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 { + 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); } public JsonSchemaRef getSchemaRef() { return this.schema; } - @Override public void preloadJsonSchema() { JsonSchema jsonSchema = null; @@ -145,14 +134,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/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 88942b85c..1959231e3 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} that resolves $ref. + */ public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); @@ -76,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); @@ -166,50 +169,39 @@ private static JsonSchema getJsonSchema(JsonSchema parent, @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - Set errors = Collections.emptySet(); - 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 { + debug(logger, node, rootNode, instanceLocation); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.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) { - Set errors = Collections.emptySet(); - 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 { + 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.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; @@ -227,14 +219,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(); } } From 81d19afcb70b0b59f1e4ee23abe6fc1c76edd378 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 15:41:56 +0800 Subject: [PATCH 42/53] Update docs --- README.md | 27 +++++++++++++++++++++------ doc/compatibility.md | 6 +++--- doc/upgrading.md | 2 +- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c355e9735..c332e39b3 100644 --- a/README.md +++ b/README.md @@ -19,18 +19,21 @@ In addition, it also works for OpenAPI 3.0 request/response validation with some Information on the compatibility support for each version, including known issues, can be found in the [Compatibility with JSON Schema versions](doc/compatibility.md) document. +Since [Draft 2019-09](https://json-schema.org/draft/2019-09/json-schema-validation#rfc.section.7) the `format` keyword only generates annotations by default and does not generate assertions. + +This behavior can be overridden to generate assertions by setting the `setFormatAssertionsEnabled` to `true` in `SchemaValidatorsConfig` or `ExecutionConfig`. + ## Upgrading to new versions -This library can contain breaking changes in minor version releases that may require code changes. +This library can contain breaking changes in `minor` version releases that may require code changes. Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. -Information on the latest version can be found on the [Releases](https://github.com/networknt/json-schema-validator/releases) page. +The [Releases](https://github.com/networknt/json-schema-validator/releases) page will contain information on the latest versions. ## Comparing against other implementations -The [JSON Schema Validation Comparison -](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. +The [JSON Schema Validation Comparison](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. * [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional#summary-results-table) * [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) @@ -434,12 +437,24 @@ The following is sample output from the Hierarchical format. | Name | Description | Default Value |--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- -| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` | `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` | `annotationCollectionPredicate`| The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` -| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` +| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` | `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaValidatorsConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` + +### Schema Validators Configuration +| Name | Description | Default Value +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- +| `pathType` | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_POINTER` to use JSON Pointer. | `PathType.DEFAULT` +| `ecma262Validator` | Whether to use the ECMA 262 `joni` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown. | `false` +| `executionContextCustomizer` | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run. | `null` +| `schemaIdValidator` | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema. | `JsonSchemaIdValidator.DEFAULT` +| `messageSource` | This is used to retrieve the locale specific messages. | `DefaultMessageSource.getInstance()` +| `locale` | The locale to use for generating messages in the `ValidationMessage`. | `Locale.getDefault()` +| `failFast` | Whether to return failure immediately when an assertion is generated. | `false` +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` ## Performance Considerations diff --git a/doc/compatibility.md b/doc/compatibility.md index 35f0bab4b..60b0fbb3e 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -115,7 +115,7 @@ This also requires adding the `joni` dependency. ``` -### Format +#### Format Since Draft 2019-09 the `format` keyword only generates annotations by default and does not generate assertions. @@ -126,7 +126,7 @@ This can be configured on a schema basis by using a meta schema with the appropr | Draft 2019-09 | `https://json-schema.org/draft/2019-09/vocab/format` | `true` | | Draft 2020-12 | `https://json-schema.org/draft/2020-12/vocab/format-assertion`| `true`/`false` | -This behavior can be overridden to generate assertions on a per-execution basis by setting the `setFormatAssertionsEnabled` to `true`. +This behavior can be overridden to generate assertions by setting the `setFormatAssertionsEnabled` option to `true`. | Format | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | |:----------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| @@ -150,7 +150,7 @@ This behavior can be overridden to generate assertions on a per-execution basis | uri-template | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | | uuid | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | -### Footnotes +##### Footnotes 1. Note that the validation are only optional for some of the keywords/formats. 2. Refer to the corresponding JSON schema for more information on whether the keyword/format is optional or not. diff --git a/doc/upgrading.md b/doc/upgrading.md index de02672fd..6da6ae54b 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -1,6 +1,6 @@ ## Upgrading to new versions -This library can contain breaking changes in minor version releases. +This library can contain breaking changes in `minor` version releases. This contains information on the notable or breaking changes in each version. From b741af2248688be0db980eb1a02c9def5abc9341 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 16:22:03 +0800 Subject: [PATCH 43/53] Add 857 test --- .../com/networknt/schema/Issue857Test.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/test/java/com/networknt/schema/Issue857Test.java diff --git a/src/test/java/com/networknt/schema/Issue857Test.java b/src/test/java/com/networknt/schema/Issue857Test.java new file mode 100644 index 000000000..8d533e5ea --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue857Test.java @@ -0,0 +1,57 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue857Test { + @Test + void test() { + String schema = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"properties\": {\r\n" + + " \"id\": {\r\n" + + " \"not\": {\r\n" + + " \"enum\": [\r\n" + + " \"1\",\r\n" + + " \"2\",\r\n" + + " \"3\"\r\n" + + " ]\r\n" + + " },\r\n" + + " \"type\": \"string\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"$id\": \"https://d73abc/filter.json\"\r\n" + + "}"; + + String input = "{\r\n" + + " \"id\": \"4\"\r\n" + + "}"; + + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setFailFast(true); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + Set result = factory.getSchema(schema, config).validate(input, InputFormat.JSON); + assertTrue(result.isEmpty()); + } +} From 8095bbe269802918a7511c3bea1ec8abd7059314 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 17:03:11 +0800 Subject: [PATCH 44/53] Javadoc --- .../networknt/schema/AbstractCollector.java | 5 + .../schema/AbstractJsonValidator.java | 16 + .../schema/AdditionalPropertiesValidator.java | 3 + .../com/networknt/schema/AllOfValidator.java | 71 +++-- .../com/networknt/schema/AnyOfValidator.java | 3 + .../com/networknt/schema/ConstValidator.java | 3 + .../networknt/schema/ContainsValidator.java | 3 + .../schema/ContentEncodingValidator.java | 12 +- .../schema/ContentMediaTypeValidator.java | 11 +- .../schema/DependenciesValidator.java | 12 + .../networknt/schema/DependentRequired.java | 3 + .../networknt/schema/DependentSchemas.java | 3 + .../com/networknt/schema/EnumValidator.java | 3 + .../schema/ExclusiveMaximumValidator.java | 3 + .../schema/ExclusiveMinimumValidator.java | 3 + .../schema/FailFastAssertionException.java | 15 + .../com/networknt/schema/FalseValidator.java | 3 + .../java/com/networknt/schema/Format.java | 10 + .../com/networknt/schema/IfValidator.java | 3 + .../com/networknt/schema/ItemsValidator.java | 3 + .../schema/ItemsValidator202012.java | 3 + .../networknt/schema/MaxItemsValidator.java | 4 +- .../networknt/schema/MaxLengthValidator.java | 3 + .../schema/MaxPropertiesValidator.java | 3 + .../networknt/schema/MaximumValidator.java | 3 + .../networknt/schema/MinItemsValidator.java | 3 + .../networknt/schema/MinLengthValidator.java | 3 + .../schema/MinMaxContainsValidator.java | 2 +- .../schema/MinPropertiesValidator.java | 3 + .../networknt/schema/MinimumValidator.java | 3 + .../networknt/schema/MultipleOfValidator.java | 3 + .../networknt/schema/NotAllowedValidator.java | 3 + .../com/networknt/schema/NotValidator.java | 3 + .../com/networknt/schema/OneOfValidator.java | 3 + .../schema/PatternPropertiesValidator.java | 3 + .../schema/PrefixItemsValidator.java | 3 + .../networknt/schema/PropertiesValidator.java | 3 + .../networknt/schema/ReadOnlyValidator.java | 3 + .../networknt/schema/RequiredValidator.java | 3 + .../com/networknt/schema/TrueValidator.java | 3 + .../com/networknt/schema/TypeValidator.java | 3 + .../schema/UnevaluatedItemsValidator.java | 281 +++++++++--------- .../UnevaluatedPropertiesValidator.java | 169 +++++------ .../networknt/schema/UnionTypeValidator.java | 3 + .../schema/UniqueItemsValidator.java | 3 + .../schema/ValidationMessageHandler.java | 2 - .../networknt/schema/WriteOnlyValidator.java | 3 + .../schema/format/AbstractFormat.java | 6 + .../schema/format/AbstractRFC3986Format.java | 15 + .../schema/format/DateTimeValidator.java | 3 + .../schema/i18n/DefaultMessageSource.java | 6 + .../com/networknt/schema/Issue456Test.java | 2 +- .../schema/ThresholdMixinPerfTest.java | 2 +- 53 files changed, 464 insertions(+), 282 deletions(-) diff --git a/src/main/java/com/networknt/schema/AbstractCollector.java b/src/main/java/com/networknt/schema/AbstractCollector.java index cb0cf4bcc..3a45d81d0 100644 --- a/src/main/java/com/networknt/schema/AbstractCollector.java +++ b/src/main/java/com/networknt/schema/AbstractCollector.java @@ -15,6 +15,11 @@ */ package com.networknt.schema; +/** + * Base collector. + * + * @param the type + */ public abstract class AbstractCollector implements Collector { @Override diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index 2da3983d5..b0552a6ff 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -21,12 +21,23 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +/** + * Base {@link JsonValidator}. + */ public abstract class AbstractJsonValidator implements JsonValidator { private final SchemaLocation schemaLocation; private final JsonNode schemaNode; private final JsonNodePath evaluationPath; private final Keyword keyword; + /** + * Constructor. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param keyword the keyword + * @param schemaNode the schema node + */ public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword, JsonNode schemaNode) { this.schemaLocation = schemaLocation; this.evaluationPath = evaluationPath; @@ -49,6 +60,11 @@ public String getKeyword() { return keyword.getValue(); } + /** + * The schema node used to create the validator. + * + * @return the schema node + */ public JsonNode getSchemaNode() { return this.schemaNode; } diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index e7decbba1..ce05c5e66 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -25,6 +25,9 @@ import java.util.*; +/** + * {@link JsonValidator} for additionalProperties. + */ public class AdditionalPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AdditionalPropertiesValidator.class); diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index e6586d7ce..8442b47d6 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -24,6 +24,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * {@link JsonValidator} for allOf. + */ public class AllOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AllOfValidator.class); @@ -48,49 +51,43 @@ public Set validate(ExecutionContext executionContext, JsonNo Set childSchemaErrors = new LinkedHashSet<>(); for (JsonSchema schema : this.schemas) { - Set localErrors = new HashSet<>(); + Set localErrors = null; - try { - if (!state.isWalkEnabled()) { - localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); - } else { - localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); - } + if (!state.isWalkEnabled()) { + localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + } else { + localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); + } - childSchemaErrors.addAll(localErrors); - - if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - final Iterator arrayElements = this.schemaNode.elements(); - while (arrayElements.hasNext()) { - final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); - final JsonNode $ref = allOfEntry.get("$ref"); - if (null != $ref) { - final DiscriminatorContext currentDiscriminatorContext = executionContext - .getCurrentDiscriminatorContext(); - if (null != currentDiscriminatorContext) { - final ObjectNode discriminator = currentDiscriminatorContext - .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); - if (null != discriminator) { - registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, this.parentSchema, instanceLocation); - // now we have to check whether we have hit the right target - final String discriminatorPropertyName = discriminator.get("propertyName").asText(); - final JsonNode discriminatorNode = node.get(discriminatorPropertyName); - final String discriminatorPropertyValue = discriminatorNode == null - ? null - : discriminatorNode.textValue(); - - final JsonSchema jsonSchema = this.parentSchema; - checkDiscriminatorMatch( - currentDiscriminatorContext, - discriminator, - discriminatorPropertyValue, - jsonSchema); - } + childSchemaErrors.addAll(localErrors); + + if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + final Iterator arrayElements = this.schemaNode.elements(); + while (arrayElements.hasNext()) { + final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); + final JsonNode $ref = allOfEntry.get("$ref"); + if (null != $ref) { + final DiscriminatorContext currentDiscriminatorContext = executionContext + .getCurrentDiscriminatorContext(); + if (null != currentDiscriminatorContext) { + final ObjectNode discriminator = currentDiscriminatorContext + .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); + if (null != discriminator) { + registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, + this.parentSchema, instanceLocation); + // now we have to check whether we have hit the right target + final String discriminatorPropertyName = discriminator.get("propertyName").asText(); + final JsonNode discriminatorNode = node.get(discriminatorPropertyName); + final String discriminatorPropertyValue = discriminatorNode == null ? null + : discriminatorNode.textValue(); + + final JsonSchema jsonSchema = this.parentSchema; + checkDiscriminatorMatch(currentDiscriminatorContext, discriminator, + discriminatorPropertyValue, jsonSchema); } } } } - } finally { } } diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index c21519052..f99cb7925 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -24,6 +24,9 @@ import java.util.*; import java.util.stream.Collectors; +/** + * {@link JsonValidator} for anyOf. + */ public class AnyOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AnyOfValidator.class); private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation"; diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index ceb34c8ff..8c8151f24 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for const. + */ public class ConstValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(ConstValidator.class); JsonNode schemaNode; diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index 0bec642d5..00c177e08 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -31,6 +31,9 @@ import static com.networknt.schema.VersionCode.MinV201909; +/** + * {@link JsonValidator} for contains. + */ public class ContainsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ContainsValidator.class); private static final String CONTAINS_MAX = "contains.max"; diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java index addb0dbfb..03a51c31f 100644 --- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -24,9 +24,8 @@ 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. @@ -35,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, diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java index 8e21cfdeb..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); diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index e5fc60629..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); diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index 62c9f4fc1..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>(); 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/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 492d79b42..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); diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index d3ab8bb8a..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); diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index 908a2f02d..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); diff --git a/src/main/java/com/networknt/schema/FailFastAssertionException.java b/src/main/java/com/networknt/schema/FailFastAssertionException.java index 6331add69..6ea3cf455 100644 --- a/src/main/java/com/networknt/schema/FailFastAssertionException.java +++ b/src/main/java/com/networknt/schema/FailFastAssertionException.java @@ -34,14 +34,29 @@ public class FailFastAssertionException extends RuntimeException { 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); } diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index b6b77860b..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); 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/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 77d387e3f..f6bacb795 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -23,6 +23,9 @@ import java.util.*; +/** + * {@link JsonValidator} for if. + */ public class IfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(IfValidator.class); diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index ce23f1faa..0ba438f11 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -27,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"; diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index f94783578..459867c83 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -27,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); diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index b79a086c2..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) { diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index ad95d50fd..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); diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index 9b8c21693..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); diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index 85fe78bb2..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"; diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 5c4aa4bc4..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); diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index a0013855c..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); diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index ef2f91fd6..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} diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index da6f722a3..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); diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 589772fc2..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"; diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 0e94fcf18..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); diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index e3988b799..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); diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index e8d88edf6..9a2c2e36e 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -23,6 +23,9 @@ import java.util.*; +/** + * {@link JsonValidator} for not. + */ public class NotValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(NotValidator.class); diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 5638759bc..1fe74a56e 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -23,6 +23,9 @@ import java.util.*; +/** + * {@link JsonValidator} for oneOf. + */ public class OneOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(OneOfValidator.class); diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index 08d945c47..1e5bc18c9 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -24,6 +24,9 @@ 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); diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index 27890b93a..230f67798 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -30,6 +30,9 @@ 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); diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index a5fc1171b..f3ce77719 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -27,6 +27,9 @@ 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); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index 462e228d4..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); diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 74390fc00..131bd42fe 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} for required. + */ public class RequiredValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class); diff --git a/src/main/java/com/networknt/schema/TrueValidator.java b/src/main/java/com/networknt/schema/TrueValidator.java index 908ed483b..0c003d94d 100644 --- a/src/main/java/com/networknt/schema/TrueValidator.java +++ b/src/main/java/com/networknt/schema/TrueValidator.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for true. + */ public class TrueValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(TrueValidator.class); diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 5a2bf9fd0..fb151c9cb 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -23,6 +23,9 @@ import java.util.*; +/** + * {@link JsonValidator} for type. + */ public class TypeValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(TypeValidator.class); diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index e190410e9..b75881b83 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -29,6 +29,9 @@ import static com.networknt.schema.VersionCode.MinV202012; +/** + * {@link JsonValidator} for unevaluatedItems. + */ public class UnevaluatedItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedItemsValidator.class); @@ -57,167 +60,151 @@ public Set validate(ExecutionContext executionContext, JsonNo } debug(logger, node, rootNode, instanceLocation); - try { - /* - * Keywords renamed in 2020-12 - * - * items -> prefixItems additionalItems -> items - */ - String itemsKeyword = isMinV202012 ? "prefixItems" : "items"; - String additionalItemsKeyword = isMinV202012 ? "items" : "additionalItems"; - - boolean valid = false; - int validCount = 0; - - // This indicates whether the "unevaluatedItems" subschema was used for - // evaluated for setting the annotation - boolean evaluated = false; - - // Get all the valid adjacent annotations - Predicate validEvaluationPathFilter = a -> { - return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); - }; - - Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() - .startsWith(this.evaluationPath.getParent()); - - List instanceLocationAnnotations = executionContext - .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyList()); - - // If schema is "unevaluatedItems: true" this is valid - if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { - valid = true; - // No need to actually evaluate since the schema is true but if there are any - // items the annotation needs to be set - if (node.size() > 0) { - evaluated = true; - } + /* + * Keywords renamed in 2020-12 + * + * items -> prefixItems additionalItems -> items + */ + String itemsKeyword = isMinV202012 ? "prefixItems" : "items"; + String additionalItemsKeyword = isMinV202012 ? "items" : "additionalItems"; + + boolean valid = false; + int validCount = 0; + + // This indicates whether the "unevaluatedItems" subschema was used for + // evaluated for setting the annotation + boolean evaluated = false; + + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + List instanceLocationAnnotations = executionContext.getAnnotations().asMap() + .getOrDefault(instanceLocation, Collections.emptyList()); + + // If schema is "unevaluatedItems: true" this is valid + if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { + valid = true; + // No need to actually evaluate since the schema is true but if there are any + // items the annotation needs to be set + if (node.size() > 0) { + evaluated = true; + } + } else { + // Get all the "items" for the instanceLocation + List items = instanceLocationAnnotations.stream() + .filter(a -> itemsKeyword.equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + if (items.isEmpty()) { + // The "items" wasn't applied meaning it is unevaluated if there is content + valid = false; } else { - // Get all the "items" for the instanceLocation - List items = instanceLocationAnnotations - .stream() - .filter(a -> itemsKeyword.equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter) - .filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - if (items.isEmpty()) { - // The "items" wasn't applied meaning it is unevaluated if there is content - valid = false; - } else { - // Annotation results for "items" keywords from multiple schemas applied to the - // same instance location are combined by setting the combined result to true if - // any of the values are true, and otherwise retaining the largest numerical - // value. - for (JsonNodeAnnotation annotation : items) { - if (annotation.getValue() instanceof Number) { - Number value = annotation.getValue(); - int existing = value.intValue(); - if (existing > validCount) { - validCount = existing; - } - } else if (annotation.getValue() instanceof Boolean) { - // The annotation "items: true" - valid = true; + // Annotation results for "items" keywords from multiple schemas applied to the + // same instance location are combined by setting the combined result to true if + // any of the values are true, and otherwise retaining the largest numerical + // value. + for (JsonNodeAnnotation annotation : items) { + if (annotation.getValue() instanceof Number) { + Number value = annotation.getValue(); + int existing = value.intValue(); + if (existing > validCount) { + validCount = existing; } + } else if (annotation.getValue() instanceof Boolean) { + // The annotation "items: true" + valid = true; } } - if (!valid) { - // Check the additionalItems annotation - // If the "additionalItems" subschema is applied to any positions within the - // instance array, it produces an annotation result of boolean true, analogous - // to the single schema behavior of "items". If any "additionalItems" keyword - // from any subschema applied to the same instance location produces an - // annotation value of true, then the combined result from these keywords is - // also true. - List additionalItems = instanceLocationAnnotations - .stream() - .filter(a -> additionalItemsKeyword.equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter) - .filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : additionalItems) { - if (annotation.getValue() instanceof Boolean - && Boolean.TRUE.equals(annotation.getValue())) { - // The annotation "additionalItems: true" - valid = true; - } - } - } - if (!valid) { - // Unevaluated - // Check if there are any "unevaluatedItems" annotations - List unevaluatedItems = instanceLocationAnnotations - .stream() - .filter(a -> "unevaluatedItems".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter) - .filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : unevaluatedItems) { - if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { - // The annotation "unevaluatedItems: true" - valid = true; - } + } + if (!valid) { + // Check the additionalItems annotation + // If the "additionalItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "additionalItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + List additionalItems = instanceLocationAnnotations.stream() + .filter(a -> additionalItemsKeyword.equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalItems) { + if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "additionalItems: true" + valid = true; } } } - Set messages = null; if (!valid) { - // Get all the "contains" for the instanceLocation - List contains = instanceLocationAnnotations - .stream() - .filter(a -> "contains".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter) - .filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - - Set containsEvaluated = new HashSet<>(); - boolean containsEvaluatedAll = false; - for (JsonNodeAnnotation a : contains) { - if (a.getValue() instanceof List) { - List values = a.getValue(); - containsEvaluated.addAll(values); - } else if (a.getValue() instanceof Boolean) { - containsEvaluatedAll = true; + // Unevaluated + // Check if there are any "unevaluatedItems" annotations + List unevaluatedItems = instanceLocationAnnotations.stream() + .filter(a -> "unevaluatedItems".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedItems) { + if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "unevaluatedItems: true" + valid = true; } } - - messages = new LinkedHashSet<>(); - if (!containsEvaluatedAll) { - // Start evaluating from the valid count - for (int x = validCount; x < node.size(); x++) { - // The schema is either "false" or an object schema - if (!containsEvaluated.contains(x)) { - messages.addAll(this.schema.validate(executionContext, node.get(x), node, - instanceLocation.append(x))); - evaluated = true; - } - } + } + } + Set messages = null; + if (!valid) { + // Get all the "contains" for the instanceLocation + List contains = instanceLocationAnnotations.stream() + .filter(a -> "contains".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + + Set containsEvaluated = new HashSet<>(); + boolean containsEvaluatedAll = false; + for (JsonNodeAnnotation a : contains) { + if (a.getValue() instanceof List) { + List values = a.getValue(); + containsEvaluated.addAll(values); + } else if (a.getValue() instanceof Boolean) { + containsEvaluatedAll = true; } - if (messages.isEmpty()) { - valid = true; - } else { - // Report these as unevaluated paths or not matching the unevalutedItems schema - messages = messages.stream() - .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) - .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.getExecutionConfig().isFailFast()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); + } + + messages = new LinkedHashSet<>(); + if (!containsEvaluatedAll) { + // Start evaluating from the valid count + for (int x = validCount; x < node.size(); x++) { + // The schema is either "false" or an object schema + if (!containsEvaluated.contains(x)) { + messages.addAll( + this.schema.validate(executionContext, node.get(x), node, instanceLocation.append(x))); + evaluated = true; + } } } - // If the "unevaluatedItems" subschema is applied to any positions within the - // instance array, it produces an annotation result of boolean true, analogous - // to the single schema behavior of "items". If any "unevaluatedItems" keyword - // from any subschema applied to the same instance location produces an - // annotation value of true, then the combined result from these keywords is - // also true. - if (evaluated) { - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword("unevaluatedItems").value(true).build()); + if (messages.isEmpty()) { + valid = true; + } else { + // Report these as unevaluated paths or not matching the unevalutedItems schema + messages = messages.stream() + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } - return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; - } finally { } + // If the "unevaluatedItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "unevaluatedItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + if (evaluated) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("unevaluatedItems").value(true).build()); + } + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index 5b408e529..441afa3ad 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -26,6 +26,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * {@link JsonValidator} for unevaluatedProperties. + */ public class UnevaluatedPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedPropertiesValidator.class); @@ -48,104 +51,92 @@ public Set validate(ExecutionContext executionContext, JsonNo } debug(logger, node, rootNode, instanceLocation); - try { - // Get all the valid adjacent annotations - Predicate validEvaluationPathFilter = a -> { - return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); - }; - - Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() - .startsWith(this.evaluationPath.getParent()); - - List instanceLocationAnnotations = executionContext - .getAnnotations().asMap().getOrDefault(instanceLocation, Collections.emptyList()); - - Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema - Set existingEvaluatedProperties = new LinkedHashSet<>(); - // Get all the "properties" for the instanceLocation - List properties = instanceLocationAnnotations - .stream() - .filter(a -> "properties".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : properties) { - if (annotation.getValue() instanceof Set) { - Set p = annotation.getValue(); - existingEvaluatedProperties.addAll(p); - } + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + List instanceLocationAnnotations = executionContext.getAnnotations().asMap() + .getOrDefault(instanceLocation, Collections.emptyList()); + + Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema + Set existingEvaluatedProperties = new LinkedHashSet<>(); + // Get all the "properties" for the instanceLocation + List properties = instanceLocationAnnotations.stream() + .filter(a -> "properties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : properties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } + } - // Get all the "patternProperties" for the instanceLocation - List patternProperties = instanceLocationAnnotations - .stream() - .filter(a -> "patternProperties".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : patternProperties) { - if (annotation.getValue() instanceof Set) { - Set p = annotation.getValue(); - existingEvaluatedProperties.addAll(p); - } + // Get all the "patternProperties" for the instanceLocation + List patternProperties = instanceLocationAnnotations.stream() + .filter(a -> "patternProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : patternProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } + } - // Get all the "patternProperties" for the instanceLocation - List additionalProperties = instanceLocationAnnotations - .stream() - .filter(a -> "additionalProperties".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : additionalProperties) { - if (annotation.getValue() instanceof Set) { - Set p = annotation.getValue(); - existingEvaluatedProperties.addAll(p); - } + // Get all the "patternProperties" for the instanceLocation + List additionalProperties = instanceLocationAnnotations.stream() + .filter(a -> "additionalProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } + } - // Get all the "unevaluatedProperties" for the instanceLocation - List unevaluatedProperties = instanceLocationAnnotations - .stream() - .filter(a -> "unevaluatedProperties".equals(a.getKeyword())) - .filter(adjacentEvaluationPathFilter).filter(validEvaluationPathFilter) - .collect(Collectors.toList()); - for (JsonNodeAnnotation annotation : unevaluatedProperties) { - if (annotation.getValue() instanceof Set) { - Set p = annotation.getValue(); - existingEvaluatedProperties.addAll(p); - } + // Get all the "unevaluatedProperties" for the instanceLocation + List unevaluatedProperties = instanceLocationAnnotations.stream() + .filter(a -> "unevaluatedProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } - - Set messages = new LinkedHashSet<>(); - for (Iterator it = node.fieldNames(); it.hasNext();) { - String fieldName = it.next(); - if (!existingEvaluatedProperties.contains(fieldName)) { - evaluatedProperties.add(fieldName); - if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { - // All fails as "unevaluatedProperties: false" - messages.add(message().instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) - .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.getExecutionConfig().isFailFast()).build()); - } else { - messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, - instanceLocation.append(fieldName))); - } + } + + Set messages = new LinkedHashSet<>(); + for (Iterator it = node.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + if (!existingEvaluatedProperties.contains(fieldName)) { + evaluatedProperties.add(fieldName); + if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { + // All fails as "unevaluatedProperties: false" + messages.add(message().instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); + } else { + messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, + instanceLocation.append(fieldName))); } } - if (!messages.isEmpty()) { - // Report these as unevaluated paths or not matching the unevaluatedProperties - // schema - messages = messages.stream() - .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) - .locale(executionContext.getExecutionConfig().getLocale()) - .failFast(executionContext.getExecutionConfig().isFailFast()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } - executionContext.getAnnotations() - .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) - .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) - .keyword(getKeyword()).value(evaluatedProperties).build()); - - return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; - } finally { } + if (!messages.isEmpty()) { + // Report these as unevaluated paths or not matching the unevaluatedProperties + // schema + messages = messages.stream() + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(evaluatedProperties).build()); + + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; } } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 3b5e898b5..630e34d30 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.Set; +/** + * {@link JsonValidator} for type union. + */ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnionTypeValidator.class); diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 0ba3aa8c6..dd8c5c6e0 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -24,6 +24,9 @@ import java.util.HashSet; import java.util.Set; +/** + * {@link JsonValidator} for uniqueItems. + */ public class UniqueItemsValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UniqueItemsValidator.class); diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 3804e4aba..1bacaffdb 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -5,11 +5,9 @@ import com.networknt.schema.utils.StringUtils; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; -import java.util.Set; public abstract class ValidationMessageHandler { protected final MessageSource messageSource; diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index b05a24428..ed2ce4ae9 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -8,6 +8,9 @@ import com.fasterxml.jackson.databind.JsonNode; +/** + * {@link JsonValidator} for writeOnly. + */ public class WriteOnlyValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(WriteOnlyValidator.class); diff --git a/src/main/java/com/networknt/schema/format/AbstractFormat.java b/src/main/java/com/networknt/schema/format/AbstractFormat.java index d0b9f270b..52f7fff37 100644 --- a/src/main/java/com/networknt/schema/format/AbstractFormat.java +++ b/src/main/java/com/networknt/schema/format/AbstractFormat.java @@ -22,6 +22,12 @@ * Used for Formats that do not need to use the {@link ExecutionContext}. */ public abstract class AbstractFormat extends BaseFormat { + /** + * Constructor. + * + * @param name the name + * @param errorMessageDescription the error message description + */ public AbstractFormat(String name, String errorMessageDescription) { super(name, errorMessageDescription); } diff --git a/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java b/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java index c839bd3e3..9b68b9962 100644 --- a/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java +++ b/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java @@ -3,8 +3,17 @@ import java.net.URI; import java.net.URISyntaxException; +/** + * {@link AbstractFormat} for RFC 3986. + */ public abstract class AbstractRFC3986Format extends AbstractFormat { + /** + * Constructor. + * + * @param name the format name + * @param errorMessageDescription the error message description + */ public AbstractRFC3986Format(String name, String errorMessageDescription) { super(name, errorMessageDescription); } @@ -19,6 +28,12 @@ public final boolean matches(String value) { } } + /** + * Determines if the uri matches the format. + * + * @param uri the uri to match + * @return true if matches + */ protected abstract boolean validate(URI uri); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 0e61f258f..5d979893e 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -35,6 +35,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link BaseFormatJsonValidator} for format for date-time. + */ public class DateTimeValidator extends BaseFormatJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class); private static final String DATETIME = "date-time"; diff --git a/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java index 79acb5b53..e62a73c5a 100644 --- a/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java +++ b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java @@ -19,8 +19,14 @@ * The default {@link MessageSource} singleton. */ public class DefaultMessageSource { + /** + * The bundle base name. + */ public static final String BUNDLE_BASE_NAME = "jsv-messages"; + /** + * The holder. + */ public static class Holder { private static final MessageSource INSTANCE = new ResourceBundleMessageSource(BUNDLE_BASE_NAME); } diff --git a/src/test/java/com/networknt/schema/Issue456Test.java b/src/test/java/com/networknt/schema/Issue456Test.java index c96c389d0..723637fd8 100644 --- a/src/test/java/com/networknt/schema/Issue456Test.java +++ b/src/test/java/com/networknt/schema/Issue456Test.java @@ -24,7 +24,7 @@ protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exce public void shouldWorkT2() throws Exception { String schemaPath = "/schema/issue456-v7.json"; String dataPath = "/data/issue456-T2.json"; - String dataT3Path = "/data/issue456-T3.json"; +// String dataT3Path = "/data/issue456-T3.json"; InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath); JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream); InputStream dataInputStream = getClass().getResourceAsStream(dataPath); diff --git a/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java b/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java index 554e40971..aea1ca49b 100644 --- a/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java +++ b/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java @@ -330,7 +330,7 @@ public String thresholdValue() { }; private double getAvgTimeViaMixin(ThresholdMixin mixin, JsonNode value, int iterations) { - boolean excludeEqual = false; +// boolean excludeEqual = false; long totalTime = 0; for (int i = 0; i < iterations; i++) { long start = System.nanoTime(); From 20ccb9417f3d9c2d68f2013cc8e98f22876da61f Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:58:44 +0800 Subject: [PATCH 45/53] Add 927 test --- .../java/com/networknt/schema/JsonSchema.java | 4 +- .../com/networknt/schema/Issue927Test.java | 135 ++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/networknt/schema/Issue927Test.java diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index b07f3c121..33882fda3 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -103,7 +103,9 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc if (hasNoFragment(schemaLocation)) { 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); diff --git a/src/test/java/com/networknt/schema/Issue927Test.java b/src/test/java/com/networknt/schema/Issue927Test.java new file mode 100644 index 000000000..5a56e2c8d --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue927Test.java @@ -0,0 +1,135 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * Test that the code isn't confused by an anchor in the id. + */ +public class Issue927Test { + @Test + void test() throws JsonMappingException, JsonProcessingException { + String schema = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"$id\": \"id\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"anyOf\": [\r\n" + + " {\r\n" + + " \"required\": [\r\n" + + " \"id\",\r\n" + + " \"type\",\r\n" + + " \"genericSubmission\"\r\n" + + " ]\r\n" + + " }\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"id\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\"\r\n" + + " },\r\n" + + " \"type\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\"\r\n" + + " },\r\n" + + " \"genericSubmission\": {\r\n" + + " \"$ref\": \"#/definitions/genericSubmission\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"definitions\": {\r\n" + + " \"genericSubmission\": {\r\n" + + " \"$id\": \"#/definitions/genericSubmission\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"required\": [\r\n" + + " \"transactionReference\",\r\n" + + " \"title\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"transactionReference\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"description\": \"description\"\r\n" + + " },\r\n" + + " \"title\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 1,\r\n" + + " \"items\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\r\n" + + " \"value\",\r\n" + + " \"locale\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"value\": {\r\n" + + " \"$ref\": \"#/definitions/value\"\r\n" + + " },\r\n" + + " \"locale\": {\r\n" + + " \"$ref\": \"#/definitions/locale\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " },\r\n" + + " \"value\": {\r\n" + + " \"$id\": \"#/definitions/value\",\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"locale\": {\r\n" + + " \"$id\": \"#/definitions/locale\",\r\n" + + " \"type\": \"string\",\r\n" + + " \"default\": \"fr\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7) + .getSchema(SchemaLocation.of("http://www.example.org"), JsonMapperFactory.getInstance().readTree(schema)); + + String input = "{\r\n" + + " \"$schema\": \"./mySchema.json\",\r\n" + + " \"_comment\": \"comment\",\r\n" + + " \"id\": \"b34024c4-6103-478c-bad6-83b26d98a892\",\r\n" + + " \"type\": \"genericSubmission\",\r\n" + + " \"genericSubmission\": {\r\n" + + " \"transactionReference\": \"123456\",\r\n" + + " \"title\": [\r\n" + + " {\r\n" + + " \"value\": \"[DE]...\",\r\n" + + " \"locale\": \"de\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"value\": \"[EN]...\",\r\n" + + " \"locale\": \"en\"\r\n" + + " }\r\n" + + " ]\r\n" + + " }\r\n" + + "}"; + Set messages = jsonSchema.validate(input, InputFormat.JSON); + assertEquals(0, messages.size()); + } + +} From 0349929642e7bbd719e1d64c7e936ccafdde2aa2 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:10:31 +0800 Subject: [PATCH 46/53] Move out non suite tests --- .../java/com/networknt/schema/PrefixItemsValidatorTest.java | 2 +- .../prefixItemsException/prefixItemsException.json | 0 .../recursiveRefException/invalidRecursiveReference.json | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/test/{suite/tests => resources}/prefixItemsException/prefixItemsException.json (100%) rename src/test/{suite/tests => resources}/recursiveRefException/invalidRecursiveReference.json (100%) diff --git a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java index 4ecbd4e37..66f350e0f 100644 --- a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java +++ b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java @@ -18,7 +18,7 @@ public class PrefixItemsValidatorTest extends AbstractJsonSchemaTestSuite { */ @Test void testEmptyPrefixItemsException() { - Stream dynamicNodeStream = createTests(SpecVersion.VersionFlag.V7, "src/test/suite/tests/prefixItemsException"); + Stream dynamicNodeStream = createTests(SpecVersion.VersionFlag.V7, "src/test/resources/prefixItemsException"); dynamicNodeStream.forEach( dynamicNode -> { assertThrows(JsonSchemaException.class, () -> { diff --git a/src/test/suite/tests/prefixItemsException/prefixItemsException.json b/src/test/resources/prefixItemsException/prefixItemsException.json similarity index 100% rename from src/test/suite/tests/prefixItemsException/prefixItemsException.json rename to src/test/resources/prefixItemsException/prefixItemsException.json diff --git a/src/test/suite/tests/recursiveRefException/invalidRecursiveReference.json b/src/test/resources/recursiveRefException/invalidRecursiveReference.json similarity index 100% rename from src/test/suite/tests/recursiveRefException/invalidRecursiveReference.json rename to src/test/resources/recursiveRefException/invalidRecursiveReference.json From d37aa90d5407d70c9f97b06ea3b83a0f1f730987 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:16:50 +0800 Subject: [PATCH 47/53] Update to latest test suite --- .../draft-next/detached-dynamicref.json | 13 ++ .../remotes/draft-next/detached-ref.json | 13 ++ .../suite/remotes/draft-next/subSchemas.json | 12 +- .../remotes/draft2019-09/detached-ref.json | 13 ++ .../remotes/draft2019-09/subSchemas.json | 12 +- .../remotes/draft2020-12/subSchemas.json | 12 +- .../suite/remotes/draft6/detached-ref.json | 13 ++ .../suite/remotes/draft7/detached-ref.json | 13 ++ src/test/suite/remotes/subSchemas.json | 12 +- src/test/suite/tests/draft-next/anchor.json | 90 -------- src/test/suite/tests/draft-next/contains.json | 104 ++------- .../tests/draft-next/dependentSchemas.json | 1 + .../suite/tests/draft-next/dynamicRef.json | 134 ++++++++--- src/test/suite/tests/draft-next/id.json | 83 ------- src/test/suite/tests/draft-next/items.json | 20 ++ .../suite/tests/draft-next/maxContains.json | 50 ---- .../suite/tests/draft-next/maxLength.json | 2 +- .../suite/tests/draft-next/minLength.json | 2 +- .../tests/draft-next/optional/anchor.json | 60 +++++ .../draft-next/optional/format/hostname.json | 25 ++ .../optional/format/idn-hostname.json | 25 ++ .../draft-next/optional/format/ipv4.json | 5 + .../suite/tests/draft-next/optional/id.json | 53 +++++ .../optional/refOfUnknownKeyword.json | 23 ++ .../{ => optional}/unknownKeyword.json | 0 src/test/suite/tests/draft-next/ref.json | 8 + .../suite/tests/draft-next/refRemote.json | 38 ++- .../tests/draft-next/unevaluatedItems.json | 73 ++++++ .../draft-next/unevaluatedProperties.json | 137 +++++++---- .../tests/draft2019-09/additionalItems.json | 20 ++ .../tests/draft2019-09/dependentSchemas.json | 1 + src/test/suite/tests/draft2019-09/id.json | 83 ------- .../suite/tests/draft2019-09/maxLength.json | 2 +- .../suite/tests/draft2019-09/minLength.json | 2 +- src/test/suite/tests/draft2019-09/not.json | 154 ++++++++++++- .../draft2019-09/optional/format/ipv4.json | 5 + src/test/suite/tests/draft2019-09/ref.json | 218 +++++++++--------- .../suite/tests/draft2019-09/refRemote.json | 38 ++- .../tests/draft2019-09/unknownKeyword.json | 57 ----- .../tests/draft2020-12/dependentSchemas.json | 1 + src/test/suite/tests/draft2020-12/id.json | 83 ------- src/test/suite/tests/draft2020-12/items.json | 20 ++ .../suite/tests/draft2020-12/maxLength.json | 2 +- .../suite/tests/draft2020-12/minLength.json | 2 +- src/test/suite/tests/draft2020-12/not.json | 154 ++++++++++++- .../draft2020-12/optional/format/ipv4.json | 5 + src/test/suite/tests/draft2020-12/ref.json | 8 + .../suite/tests/draft2020-12/refRemote.json | 39 +++- .../tests/draft2020-12/unknownKeyword.json | 57 ----- .../suite/tests/draft3/additionalItems.json | 19 ++ src/test/suite/tests/draft3/maxLength.json | 2 +- src/test/suite/tests/draft3/minLength.json | 2 +- src/test/suite/tests/draft3/refRemote.json | 4 +- .../suite/tests/draft4/additionalItems.json | 19 ++ src/test/suite/tests/draft4/maxLength.json | 2 +- src/test/suite/tests/draft4/minLength.json | 2 +- src/test/suite/tests/draft4/not.json | 63 ++++- .../suite/tests/draft4/{ => optional}/id.json | 0 src/test/suite/tests/draft4/refRemote.json | 4 +- .../suite/tests/draft6/additionalItems.json | 19 ++ src/test/suite/tests/draft6/maxLength.json | 2 +- src/test/suite/tests/draft6/minLength.json | 2 +- src/test/suite/tests/draft6/not.json | 152 +++++++++++- .../suite/tests/draft6/{ => optional}/id.json | 0 .../draft6/{ => optional}/unknownKeyword.json | 0 src/test/suite/tests/draft6/ref.json | 27 +++ src/test/suite/tests/draft6/refRemote.json | 22 +- .../suite/tests/draft7/additionalItems.json | 19 ++ src/test/suite/tests/draft7/maxLength.json | 2 +- src/test/suite/tests/draft7/minLength.json | 2 +- src/test/suite/tests/draft7/not.json | 152 +++++++++++- .../tests/draft7/optional/format/ipv4.json | 5 + .../suite/tests/draft7/{ => optional}/id.json | 0 .../draft7/{ => optional}/unknownKeyword.json | 0 src/test/suite/tests/draft7/ref.json | 27 +++ src/test/suite/tests/draft7/refRemote.json | 22 +- 76 files changed, 1718 insertions(+), 849 deletions(-) create mode 100644 src/test/suite/remotes/draft-next/detached-dynamicref.json create mode 100644 src/test/suite/remotes/draft-next/detached-ref.json create mode 100644 src/test/suite/remotes/draft2019-09/detached-ref.json create mode 100644 src/test/suite/remotes/draft6/detached-ref.json create mode 100644 src/test/suite/remotes/draft7/detached-ref.json create mode 100644 src/test/suite/tests/draft-next/optional/anchor.json create mode 100644 src/test/suite/tests/draft-next/optional/id.json rename src/test/suite/tests/draft-next/{ => optional}/unknownKeyword.json (100%) delete mode 100644 src/test/suite/tests/draft2019-09/unknownKeyword.json delete mode 100644 src/test/suite/tests/draft2020-12/unknownKeyword.json rename src/test/suite/tests/draft4/{ => optional}/id.json (100%) rename src/test/suite/tests/draft6/{ => optional}/id.json (100%) rename src/test/suite/tests/draft6/{ => optional}/unknownKeyword.json (100%) rename src/test/suite/tests/draft7/{ => optional}/id.json (100%) rename src/test/suite/tests/draft7/{ => optional}/unknownKeyword.json (100%) diff --git a/src/test/suite/remotes/draft-next/detached-dynamicref.json b/src/test/suite/remotes/draft-next/detached-dynamicref.json new file mode 100644 index 000000000..c1a09a583 --- /dev/null +++ b/src/test/suite/remotes/draft-next/detached-dynamicref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft-next/detached-dynamicref.json", + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "foo": { + "$dynamicRef": "#detached" + }, + "detached": { + "$dynamicAnchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft-next/detached-ref.json b/src/test/suite/remotes/draft-next/detached-ref.json new file mode 100644 index 000000000..d01aaa128 --- /dev/null +++ b/src/test/suite/remotes/draft-next/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft-next/detached-ref.json", + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft-next/subSchemas.json b/src/test/suite/remotes/draft-next/subSchemas.json index 575dd00c2..75b7583ca 100644 --- a/src/test/suite/remotes/draft-next/subSchemas.json +++ b/src/test/suite/remotes/draft-next/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/next/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft2019-09/detached-ref.json b/src/test/suite/remotes/draft2019-09/detached-ref.json new file mode 100644 index 000000000..4a3499fd1 --- /dev/null +++ b/src/test/suite/remotes/draft2019-09/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft2019-09/detached-ref.json", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft2019-09/subSchemas.json b/src/test/suite/remotes/draft2019-09/subSchemas.json index 6dea22525..fdfee68d9 100644 --- a/src/test/suite/remotes/draft2019-09/subSchemas.json +++ b/src/test/suite/remotes/draft2019-09/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft2020-12/subSchemas.json b/src/test/suite/remotes/draft2020-12/subSchemas.json index 5fca21d82..1bb4846d7 100644 --- a/src/test/suite/remotes/draft2020-12/subSchemas.json +++ b/src/test/suite/remotes/draft2020-12/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft6/detached-ref.json b/src/test/suite/remotes/draft6/detached-ref.json new file mode 100644 index 000000000..05ce071ba --- /dev/null +++ b/src/test/suite/remotes/draft6/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft6/detached-ref.json", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$id": "#detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft7/detached-ref.json b/src/test/suite/remotes/draft7/detached-ref.json new file mode 100644 index 000000000..27f2ec80a --- /dev/null +++ b/src/test/suite/remotes/draft7/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft7/detached-ref.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$id": "#detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/subSchemas.json b/src/test/suite/remotes/subSchemas.json index 9f8030bce..6e9b3de35 100644 --- a/src/test/suite/remotes/subSchemas.json +++ b/src/test/suite/remotes/subSchemas.json @@ -1,8 +1,10 @@ { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } } diff --git a/src/test/suite/tests/draft-next/anchor.json b/src/test/suite/tests/draft-next/anchor.json index 321d84461..a0c4c51a5 100644 --- a/src/test/suite/tests/draft-next/anchor.json +++ b/src/test/suite/tests/draft-next/anchor.json @@ -81,64 +81,6 @@ } ] }, - { - "description": "$anchor inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $anchor buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "anchor_in_enum": { - "enum": [ - { - "$anchor": "my_anchor", - "type": "null" - } - ] - }, - "real_identifier_in_schema": { - "$anchor": "my_anchor", - "type": "string" - }, - "zzz_anchor_in_const": { - "const": { - "$anchor": "my_anchor", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/anchor_in_enum" }, - { "$ref": "#my_anchor" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$anchor": "my_anchor", - "type": "null" - }, - "valid": true - }, - { - "description": "in implementations that strip $anchor, this may match either $def", - "data": { - "type": "null" - }, - "valid": false - }, - { - "description": "match $ref to $anchor", - "data": "a string to match #/$defs/anchor_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $anchor", - "data": 1, - "valid": false - } - ] - }, { "description": "same $anchor with different base uri", "schema": { @@ -175,38 +117,6 @@ } ] }, - { - "description": "non-schema object containing an $anchor property", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "const_not_anchor": { - "const": { - "$anchor": "not_a_real_anchor" - } - } - }, - "if": { - "const": "skip not_a_real_anchor" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_anchor" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_anchor", - "valid": true - }, - { - "description": "const at const_not_anchor does not match", - "data": 1, - "valid": false - } - ] - }, { "description": "invalid anchors", "schema": { diff --git a/src/test/suite/tests/draft-next/contains.json b/src/test/suite/tests/draft-next/contains.json index c17f55ee7..8539a531d 100644 --- a/src/test/suite/tests/draft-next/contains.json +++ b/src/test/suite/tests/draft-next/contains.json @@ -31,31 +31,6 @@ "data": [], "valid": false }, - { - "description": "object with property matching schema (5) is valid", - "data": { "a": 3, "b": 4, "c": 5 }, - "valid": true - }, - { - "description": "object with property matching schema (6) is valid", - "data": { "a": 3, "b": 4, "c": 6 }, - "valid": true - }, - { - "description": "object with two properties matching schema (5, 6) is valid", - "data": { "a": 3, "b": 4, "c": 5, "d": 6 }, - "valid": true - }, - { - "description": "object without properties matching schema is invalid", - "data": { "a": 2, "b": 3, "c": 4 }, - "valid": false - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false - }, { "description": "not array or object is valid", "data": 42, @@ -84,21 +59,6 @@ "description": "array without item 5 is invalid", "data": [1, 2, 3, 4], "valid": false - }, - { - "description": "object with property 5 is valid", - "data": { "a": 3, "b": 4, "c": 5 }, - "valid": true - }, - { - "description": "object with two properties 5 is valid", - "data": { "a": 3, "b": 4, "c": 5, "d": 5 }, - "valid": true - }, - { - "description": "object without property 5 is invalid", - "data": { "a": 1, "b": 2, "c": 3, "d": 4 }, - "valid": false } ] }, @@ -118,16 +78,6 @@ "description": "empty array is invalid", "data": [], "valid": false - }, - { - "description": "any non-empty object is valid", - "data": { "a": "foo" }, - "valid": true - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false } ] }, @@ -149,18 +99,28 @@ "valid": false }, { - "description": "any non-empty object is invalid", - "data": ["foo"], - "valid": false + "description": "non-arrays are valid - string", + "data": "contains does not apply to strings", + "valid": true }, { - "description": "empty object is invalid", + "description": "non-arrays are valid - object", "data": {}, - "valid": false + "valid": true }, { - "description": "non-arrays/objects are valid", - "data": "contains does not apply to strings", + "description": "non-arrays are valid - number", + "data": 42, + "valid": true + }, + { + "description": "non-arrays are valid - boolean", + "data": false, + "valid": true + }, + { + "description": "non-arrays are valid - null", + "data": null, "valid": true } ] @@ -193,26 +153,6 @@ "description": "matches neither items nor contains", "data": [1, 5], "valid": false - }, - { - "description": "matches additionalProperties, does not match contains", - "data": { "a": 2, "b": 4, "c": 8 }, - "valid": false - }, - { - "description": "does not match additionalProperties, matches contains", - "data": { "a": 3, "b": 6, "c": 9 }, - "valid": false - }, - { - "description": "matches both additionalProperties and contains", - "data": { "a": 6, "b": 12 }, - "valid": true - }, - { - "description": "matches neither additionalProperties nor contains", - "data": { "a": 1, "b": 5 }, - "valid": false } ] }, @@ -235,16 +175,6 @@ "description": "empty array is invalid", "data": [], "valid": false - }, - { - "description": "any non-empty object is valid", - "data": { "a": "foo" }, - "valid": true - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false } ] }, diff --git a/src/test/suite/tests/draft-next/dependentSchemas.json b/src/test/suite/tests/draft-next/dependentSchemas.json index 8a8477591..86079c34c 100644 --- a/src/test/suite/tests/draft-next/dependentSchemas.json +++ b/src/test/suite/tests/draft-next/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft-next/dynamicRef.json b/src/test/suite/tests/draft-next/dynamicRef.json index a4a7c4490..94124fff6 100644 --- a/src/test/suite/tests/draft-next/dynamicRef.json +++ b/src/test/suite/tests/draft-next/dynamicRef.json @@ -207,45 +207,75 @@ "schema": { "$schema": "https://json-schema.org/draft/next/schema", "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", - "$defs": { - "inner": { - "$id": "inner", - "$dynamicAnchor": "foo", - "title": "inner", - "additionalProperties": { - "$dynamicRef": "#foo" - } + "propertyDependencies": { + "kindOfList": { + "numbers": { "$ref": "numberList" }, + "strings": { "$ref": "stringList" } } }, - "if": { - "propertyNames": { - "pattern": "^[a-m]" + "$defs": { + "genericList": { + "$id": "genericList", + "properties": { + "list": { + "items": { "$dynamicRef": "#itemType" } + } + } + }, + "numberList": { + "$id": "numberList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "number" + } + }, + "$ref": "genericList" + }, + "stringList": { + "$id": "stringList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "string" + } + }, + "$ref": "genericList" } - }, - "then": { - "title": "any type of node", - "$id": "anyLeafNode", - "$dynamicAnchor": "foo", - "$ref": "inner" - }, - "else": { - "title": "integer node", - "$id": "integerNode", - "$dynamicAnchor": "foo", - "type": [ "object", "integer" ], - "$ref": "inner" } }, "tests": [ { - "description": "recurse to anyLeafNode - floats are allowed", - "data": { "alpha": 1.1 }, + "description": "number list with number values", + "data": { + "kindOfList": "numbers", + "list": [1.1] + }, "valid": true }, { - "description": "recurse to integerNode - floats are not allowed", - "data": { "november": 1.1 }, + "description": "number list with string values", + "data": { + "kindOfList": "numbers", + "list": ["foo"] + }, + "valid": false + }, + { + "description": "string list with number values", + "data": { + "kindOfList": "strings", + "list": [1.1] + }, "valid": false + }, + { + "description": "string list with string values", + "data": { + "kindOfList": "strings", + "list": ["foo"] + }, + "valid": true } ] }, @@ -564,5 +594,53 @@ "valid": false } ] + }, + { + "description": "$ref to $dynamicRef finds detached $dynamicAnchor", + "schema": { + "$ref": "http://localhost:1234/draft-next/detached-dynamicref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "$dynamicRef points to a boolean schema", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "true": true, + "false": false + }, + "properties": { + "true": { + "$dynamicRef": "#/$defs/true" + }, + "false": { + "$dynamicRef": "#/$defs/false" + } + } + }, + "tests": [ + { + "description": "follow $dynamicRef to a true schema", + "data": { "true": 1 }, + "valid": true + }, + { + "description": "follow $dynamicRef to a false schema", + "data": { "false": 1 }, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/id.json b/src/test/suite/tests/draft-next/id.json index 9b3a591f0..fe74c6bff 100644 --- a/src/test/suite/tests/draft-next/id.json +++ b/src/test/suite/tests/draft-next/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft-next/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft-next/items.json b/src/test/suite/tests/draft-next/items.json index 459943bef..dfb79af2f 100644 --- a/src/test/suite/tests/draft-next/items.json +++ b/src/test/suite/tests/draft-next/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft-next/maxContains.json b/src/test/suite/tests/draft-next/maxContains.json index 7c1515753..5af6e4c13 100644 --- a/src/test/suite/tests/draft-next/maxContains.json +++ b/src/test/suite/tests/draft-next/maxContains.json @@ -15,16 +15,6 @@ "description": "two items still valid against lone maxContains", "data": [1, 2], "valid": true - }, - { - "description": "one property valid against lone maxContains", - "data": { "a": 1 }, - "valid": true - }, - { - "description": "two properties still valid against lone maxContains", - "data": { "a": 1, "b": 2 }, - "valid": true } ] }, @@ -60,31 +50,6 @@ "description": "some elements match, invalid maxContains", "data": [1, 2, 1], "valid": false - }, - { - "description": "empty object", - "data": {}, - "valid": false - }, - { - "description": "all properties match, valid maxContains", - "data": { "a": 1 }, - "valid": true - }, - { - "description": "all properties match, invalid maxContains", - "data": { "a": 1, "b": 1 }, - "valid": false - }, - { - "description": "some properties match, valid maxContains", - "data": { "a": 1, "b": 2 }, - "valid": true - }, - { - "description": "some properties match, invalid maxContains", - "data": { "a": 1, "b": 2, "c": 1 }, - "valid": false } ] }, @@ -131,21 +96,6 @@ "description": "array with minContains < maxContains < actual", "data": [1, 1, 1, 1], "valid": false - }, - { - "description": "object with actual < minContains < maxContains", - "data": {}, - "valid": false - }, - { - "description": "object with minContains < actual < maxContains", - "data": { "a": 1, "b": 1 }, - "valid": true - }, - { - "description": "object with minContains < maxContains < actual", - "data": { "a": 1, "b": 1, "c": 1, "d": 1 }, - "valid": false } ] } diff --git a/src/test/suite/tests/draft-next/maxLength.json b/src/test/suite/tests/draft-next/maxLength.json index e09e44ad8..c88f604ef 100644 --- a/src/test/suite/tests/draft-next/maxLength.json +++ b/src/test/suite/tests/draft-next/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft-next/minLength.json b/src/test/suite/tests/draft-next/minLength.json index 16022acb5..52c9c9a14 100644 --- a/src/test/suite/tests/draft-next/minLength.json +++ b/src/test/suite/tests/draft-next/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft-next/optional/anchor.json b/src/test/suite/tests/draft-next/optional/anchor.json new file mode 100644 index 000000000..1de0b7a70 --- /dev/null +++ b/src/test/suite/tests/draft-next/optional/anchor.json @@ -0,0 +1,60 @@ +[ + { + "description": "$anchor inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $anchor buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "anchor_in_enum": { + "enum": [ + { + "$anchor": "my_anchor", + "type": "null" + } + ] + }, + "real_identifier_in_schema": { + "$anchor": "my_anchor", + "type": "string" + }, + "zzz_anchor_in_const": { + "const": { + "$anchor": "my_anchor", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/anchor_in_enum" }, + { "$ref": "#my_anchor" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$anchor": "my_anchor", + "type": "null" + }, + "valid": true + }, + { + "description": "in implementations that strip $anchor, this may match either $def", + "data": { + "type": "null" + }, + "valid": false + }, + { + "description": "match $ref to $anchor", + "data": "a string to match #/$defs/anchor_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $anchor", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft-next/optional/format/hostname.json b/src/test/suite/tests/draft-next/optional/format/hostname.json index 967848653..bfb306363 100644 --- a/src/test/suite/tests/draft-next/optional/format/hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json index ee2e792fa..109bf73c9 100644 --- a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft-next/optional/format/ipv4.json b/src/test/suite/tests/draft-next/optional/format/ipv4.json index e3e944015..2a4bc2b2f 100644 --- a/src/test/suite/tests/draft-next/optional/format/ipv4.json +++ b/src/test/suite/tests/draft-next/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft-next/optional/id.json b/src/test/suite/tests/draft-next/optional/id.json new file mode 100644 index 000000000..fc26f26c2 --- /dev/null +++ b/src/test/suite/tests/draft-next/optional/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "$id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $id buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_enum" }, + { "$ref": "https://localhost:1234/draft-next/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to $id", + "data": "a string to match #/$defs/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $id", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json b/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json index 489701cd2..c832e09f6 100644 --- a/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json +++ b/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/unknownKeyword.json b/src/test/suite/tests/draft-next/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft-next/unknownKeyword.json rename to src/test/suite/tests/draft-next/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft-next/ref.json b/src/test/suite/tests/draft-next/ref.json index 1d5f25613..8417ce299 100644 --- a/src/test/suite/tests/draft-next/ref.json +++ b/src/test/suite/tests/draft-next/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$defs": { "": { "$defs": { diff --git a/src/test/suite/tests/draft-next/refRemote.json b/src/test/suite/tests/draft-next/refRemote.json index 3768b53b6..647fb9f19 100644 --- a/src/test/suite/tests/draft-next/refRemote.json +++ b/src/test/suite/tests/draft-next/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -310,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/draft-next/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/unevaluatedItems.json b/src/test/suite/tests/draft-next/unevaluatedItems.json index 7379afb41..08f6ef128 100644 --- a/src/test/suite/tests/draft-next/unevaluatedItems.json +++ b/src/test/suite/tests/draft-next/unevaluatedItems.json @@ -461,6 +461,79 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "unevaluatedItems": false, + "prefixItems": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "prefixItems": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/unevaluated-items-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft-next/unevaluatedProperties.json b/src/test/suite/tests/draft-next/unevaluatedProperties.json index 69fe8a00c..d0d53507f 100644 --- a/src/test/suite/tests/draft-next/unevaluatedProperties.json +++ b/src/test/suite/tests/draft-next/unevaluatedProperties.json @@ -715,6 +715,92 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/unevaluated-properties-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { @@ -1365,57 +1451,6 @@ } ] }, - { - "description": "unevaluatedProperties depends on adjacent contains", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "properties": { - "foo": { "type": "number" } - }, - "contains": { "type": "string" }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "bar is evaluated by contains", - "data": { "foo": 1, "bar": "foo" }, - "valid": true - }, - { - "description": "contains fails, bar is not evaluated", - "data": { "foo": 1, "bar": 2 }, - "valid": false - }, - { - "description": "contains passes, bar is not evaluated", - "data": { "foo": 1, "bar": 2, "baz": "foo" }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties depends on multiple nested contains", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "allOf": [ - { "contains": { "multipleOf": 2 } }, - { "contains": { "multipleOf": 3 } } - ], - "unevaluatedProperties": { "multipleOf": 5 } - }, - "tests": [ - { - "description": "5 not evaluated, passes unevaluatedItems", - "data": { "a": 2, "b": 3, "c": 4, "d": 5, "e": 6 }, - "valid": true - }, - { - "description": "7 not evaluated, fails unevaluatedItems", - "data": { "a": 2, "b": 3, "c": 4, "d": 7, "e": 8 }, - "valid": false - } - ] - }, { "description": "non-object instances are valid", "schema": { diff --git a/src/test/suite/tests/draft2019-09/additionalItems.json b/src/test/suite/tests/draft2019-09/additionalItems.json index aa44bcb76..9a7ae4f8a 100644 --- a/src/test/suite/tests/draft2019-09/additionalItems.json +++ b/src/test/suite/tests/draft2019-09/additionalItems.json @@ -182,6 +182,26 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft2019-09/dependentSchemas.json b/src/test/suite/tests/draft2019-09/dependentSchemas.json index 3577efdf4..c5b8ea05f 100644 --- a/src/test/suite/tests/draft2019-09/dependentSchemas.json +++ b/src/test/suite/tests/draft2019-09/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft2019-09/id.json b/src/test/suite/tests/draft2019-09/id.json index e2e403f0b..0ba313874 100644 --- a/src/test/suite/tests/draft2019-09/id.json +++ b/src/test/suite/tests/draft2019-09/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft2019-09/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft2019-09/maxLength.json b/src/test/suite/tests/draft2019-09/maxLength.json index f242c3eff..a0cc7d9b8 100644 --- a/src/test/suite/tests/draft2019-09/maxLength.json +++ b/src/test/suite/tests/draft2019-09/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft2019-09/minLength.json b/src/test/suite/tests/draft2019-09/minLength.json index 19dec2cac..12782660c 100644 --- a/src/test/suite/tests/draft2019-09/minLength.json +++ b/src/test/suite/tests/draft2019-09/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft2019-09/not.json b/src/test/suite/tests/draft2019-09/not.json index 62c9af9de..d90728c7b 100644 --- a/src/test/suite/tests/draft2019-09/not.json +++ b/src/test/suite/tests/draft2019-09/not.json @@ -97,25 +97,173 @@ ] }, { - "description": "not with boolean schema true", + "description": "forbid everything with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": {} + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "not": true }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false } ] }, { - "description": "not with boolean schema false", + "description": "allow everything with boolean schema false", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "double negation", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": { "not": {} } + }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft2019-09/optional/format/ipv4.json b/src/test/suite/tests/draft2019-09/optional/format/ipv4.json index ac1e14c68..efe42471b 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/ipv4.json +++ b/src/test/suite/tests/draft2019-09/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft2019-09/ref.json b/src/test/suite/tests/draft2019-09/ref.json index 6d3a5cbe4..ea569908e 100644 --- a/src/test/suite/tests/draft2019-09/ref.json +++ b/src/test/suite/tests/draft2019-09/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -951,109 +955,113 @@ ] }, { - "description": "ref with absolute-path-reference", - "schema": { - "$id": "http://example.com/ref/absref.json", - "$defs": { - "a": { - "$id": "http://example.com/ref/absref/foobar.json", - "type": "number" - }, - "b": { - "$id": "http://example.com/absref/foobar.json", - "type": "string" - } - }, - "$ref": "/absref/foobar.json" - }, - "tests": [ - { - "description": "a string is valid", - "data": "foo", - "valid": true - }, - { - "description": "an integer is invalid", - "data": 12, - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - *nix", - "schema": { - "$id": "file:///folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - windows", - "schema": { - "$id": "file:///c:/folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "empty tokens in $ref json-pointer", - "schema": { - "$defs": { - "": { - "$defs": { - "": { "type": "number" } - } - } - }, - "allOf": [ - { - "$ref": "#/$defs//$defs/" - } - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - } + "description": "ref with absolute-path-reference", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "http://example.com/ref/absref.json", + "$defs": { + "a": { + "$id": "http://example.com/ref/absref/foobar.json", + "type": "number" + }, + "b": { + "$id": "http://example.com/absref/foobar.json", + "type": "string" + } + }, + "$ref": "/absref/foobar.json" + }, + "tests": [ + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "an integer is invalid", + "data": 12, + "valid": false + } + ] + }, + { + "description": "$id with file URI still resolves pointers - *nix", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "$id with file URI still resolves pointers - windows", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///c:/folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "empty tokens in $ref json-pointer", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "": { + "$defs": { + "": { "type": "number" } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs//$defs/" + } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + } ] diff --git a/src/test/suite/tests/draft2019-09/refRemote.json b/src/test/suite/tests/draft2019-09/refRemote.json index 79107f9e4..072894cf2 100644 --- a/src/test/suite/tests/draft2019-09/refRemote.json +++ b/src/test/suite/tests/draft2019-09/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -310,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/draft2019-09/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2019-09/unknownKeyword.json b/src/test/suite/tests/draft2019-09/unknownKeyword.json deleted file mode 100644 index f98e87c54..000000000 --- a/src/test/suite/tests/draft2019-09/unknownKeyword.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "description": "$id inside an unknown keyword is not a real identifier", - "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "id_in_unknown0": { - "not": { - "array_of_schemas": [ - { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "null" - } - ] - } - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "string" - }, - "id_in_unknown1": { - "not": { - "object_of_schemas": { - "foo": { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "integer" - } - } - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_unknown0" }, - { "$ref": "#/$defs/id_in_unknown1" }, - { "$ref": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "type matches second anyOf, which has a real schema in it", - "data": "a string", - "valid": true - }, - { - "description": "type matches non-schema in first anyOf", - "data": null, - "valid": false - }, - { - "description": "type matches non-schema in third anyOf", - "data": 1, - "valid": false - } - ] - } -] diff --git a/src/test/suite/tests/draft2020-12/dependentSchemas.json b/src/test/suite/tests/draft2020-12/dependentSchemas.json index 66ac0eb43..1c5f0574a 100644 --- a/src/test/suite/tests/draft2020-12/dependentSchemas.json +++ b/src/test/suite/tests/draft2020-12/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft2020-12/id.json b/src/test/suite/tests/draft2020-12/id.json index 0ae5fe68a..59265c4ec 100644 --- a/src/test/suite/tests/draft2020-12/id.json +++ b/src/test/suite/tests/draft2020-12/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft2020-12/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft2020-12/items.json b/src/test/suite/tests/draft2020-12/items.json index 1ef18bdd0..6a3e1cf26 100644 --- a/src/test/suite/tests/draft2020-12/items.json +++ b/src/test/suite/tests/draft2020-12/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft2020-12/maxLength.json b/src/test/suite/tests/draft2020-12/maxLength.json index b6eb03401..7462726d7 100644 --- a/src/test/suite/tests/draft2020-12/maxLength.json +++ b/src/test/suite/tests/draft2020-12/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft2020-12/minLength.json b/src/test/suite/tests/draft2020-12/minLength.json index e0930b6fb..5076c5a92 100644 --- a/src/test/suite/tests/draft2020-12/minLength.json +++ b/src/test/suite/tests/draft2020-12/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft2020-12/not.json b/src/test/suite/tests/draft2020-12/not.json index 57e45ba39..d0f2b6e84 100644 --- a/src/test/suite/tests/draft2020-12/not.json +++ b/src/test/suite/tests/draft2020-12/not.json @@ -97,25 +97,173 @@ ] }, { - "description": "not with boolean schema true", + "description": "forbid everything with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": {} + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "not": true }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false } ] }, { - "description": "not with boolean schema false", + "description": "allow everything with boolean schema false", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "double negation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "not": {} } + }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft2020-12/optional/format/ipv4.json b/src/test/suite/tests/draft2020-12/optional/format/ipv4.json index c72b6fc22..86d27bdb7 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/ipv4.json +++ b/src/test/suite/tests/draft2020-12/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft2020-12/ref.json b/src/test/suite/tests/draft2020-12/ref.json index 5f6be8c20..8d15fa43a 100644 --- a/src/test/suite/tests/draft2020-12/ref.json +++ b/src/test/suite/tests/draft2020-12/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { diff --git a/src/test/suite/tests/draft2020-12/refRemote.json b/src/test/suite/tests/draft2020-12/refRemote.json index f4a5b1bfb..047ac74ca 100644 --- a/src/test/suite/tests/draft2020-12/refRemote.json +++ b/src/test/suite/tests/draft2020-12/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -145,7 +145,6 @@ } } }, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -266,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -282,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -298,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -311,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2020-12/unknownKeyword.json b/src/test/suite/tests/draft2020-12/unknownKeyword.json deleted file mode 100644 index 28b0c4ce9..000000000 --- a/src/test/suite/tests/draft2020-12/unknownKeyword.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "description": "$id inside an unknown keyword is not a real identifier", - "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "id_in_unknown0": { - "not": { - "array_of_schemas": [ - { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "null" - } - ] - } - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "string" - }, - "id_in_unknown1": { - "not": { - "object_of_schemas": { - "foo": { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "integer" - } - } - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_unknown0" }, - { "$ref": "#/$defs/id_in_unknown1" }, - { "$ref": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "type matches second anyOf, which has a real schema in it", - "data": "a string", - "valid": true - }, - { - "description": "type matches non-schema in first anyOf", - "data": null, - "valid": false - }, - { - "description": "type matches non-schema in third anyOf", - "data": 1, - "valid": false - } - ] - } -] diff --git a/src/test/suite/tests/draft3/additionalItems.json b/src/test/suite/tests/draft3/additionalItems.json index 0cb668701..ab44a2eb3 100644 --- a/src/test/suite/tests/draft3/additionalItems.json +++ b/src/test/suite/tests/draft3/additionalItems.json @@ -110,6 +110,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft3/maxLength.json b/src/test/suite/tests/draft3/maxLength.json index 4de42bcab..b0a9ea5be 100644 --- a/src/test/suite/tests/draft3/maxLength.json +++ b/src/test/suite/tests/draft3/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft3/minLength.json b/src/test/suite/tests/draft3/minLength.json index 3f09158de..6652c7509 100644 --- a/src/test/suite/tests/draft3/minLength.json +++ b/src/test/suite/tests/draft3/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft3/refRemote.json b/src/test/suite/tests/draft3/refRemote.json index de0cb43a5..0e4ab53e0 100644 --- a/src/test/suite/tests/draft3/refRemote.json +++ b/src/test/suite/tests/draft3/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/src/test/suite/tests/draft4/additionalItems.json b/src/test/suite/tests/draft4/additionalItems.json index deb44fd31..c9e681549 100644 --- a/src/test/suite/tests/draft4/additionalItems.json +++ b/src/test/suite/tests/draft4/additionalItems.json @@ -146,6 +146,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft4/maxLength.json b/src/test/suite/tests/draft4/maxLength.json index 811d35b25..338795943 100644 --- a/src/test/suite/tests/draft4/maxLength.json +++ b/src/test/suite/tests/draft4/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft4/minLength.json b/src/test/suite/tests/draft4/minLength.json index 3f09158de..6652c7509 100644 --- a/src/test/suite/tests/draft4/minLength.json +++ b/src/test/suite/tests/draft4/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft4/not.json b/src/test/suite/tests/draft4/not.json index cbb7f46bf..525219cf2 100644 --- a/src/test/suite/tests/draft4/not.json +++ b/src/test/suite/tests/draft4/not.json @@ -91,6 +91,67 @@ "valid": true } ] + }, + { + "description": "forbid everything with empty schema", + "schema": { "not": {} }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "double negation", + "schema": { "not": { "not": {} } }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] } - ] diff --git a/src/test/suite/tests/draft4/id.json b/src/test/suite/tests/draft4/optional/id.json similarity index 100% rename from src/test/suite/tests/draft4/id.json rename to src/test/suite/tests/draft4/optional/id.json diff --git a/src/test/suite/tests/draft4/refRemote.json b/src/test/suite/tests/draft4/refRemote.json index 412c9ff83..64a618b89 100644 --- a/src/test/suite/tests/draft4/refRemote.json +++ b/src/test/suite/tests/draft4/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/src/test/suite/tests/draft6/additionalItems.json b/src/test/suite/tests/draft6/additionalItems.json index cae72361c..2c7d15582 100644 --- a/src/test/suite/tests/draft6/additionalItems.json +++ b/src/test/suite/tests/draft6/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft6/maxLength.json b/src/test/suite/tests/draft6/maxLength.json index 748b4daaf..be60c5407 100644 --- a/src/test/suite/tests/draft6/maxLength.json +++ b/src/test/suite/tests/draft6/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft6/minLength.json b/src/test/suite/tests/draft6/minLength.json index 64db94805..23c68fe3f 100644 --- a/src/test/suite/tests/draft6/minLength.json +++ b/src/test/suite/tests/draft6/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft6/not.json b/src/test/suite/tests/draft6/not.json index 98de0eda8..b46c4ed05 100644 --- a/src/test/suite/tests/draft6/not.json +++ b/src/test/suite/tests/draft6/not.json @@ -93,19 +93,161 @@ ] }, { - "description": "not with boolean schema true", - "schema": {"not": true}, + "description": "forbid everything with empty schema", + "schema": { "not": {} }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", + "schema": { "not": true }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "allow everything with boolean schema false", + "schema": { "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true } ] }, { - "description": "not with boolean schema false", - "schema": {"not": false}, + "description": "double negation", + "schema": { "not": { "not": {} } }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft6/id.json b/src/test/suite/tests/draft6/optional/id.json similarity index 100% rename from src/test/suite/tests/draft6/id.json rename to src/test/suite/tests/draft6/optional/id.json diff --git a/src/test/suite/tests/draft6/unknownKeyword.json b/src/test/suite/tests/draft6/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft6/unknownKeyword.json rename to src/test/suite/tests/draft6/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft6/ref.json b/src/test/suite/tests/draft6/ref.json index 8a8908a44..379322c71 100644 --- a/src/test/suite/tests/draft6/ref.json +++ b/src/test/suite/tests/draft6/ref.json @@ -445,6 +445,33 @@ } ] }, + { + "description": "Reference an anchor with a non-relative URI", + "schema": { + "$id": "https://example.com/schema-with-anchor", + "allOf": [{ + "$ref": "https://example.com/schema-with-anchor#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, { "description": "Location-independent identifier with base URI change in subschema", "schema": { diff --git a/src/test/suite/tests/draft6/refRemote.json b/src/test/suite/tests/draft6/refRemote.json index c2b200249..28459c4a0 100644 --- a/src/test/suite/tests/draft6/refRemote.json +++ b/src/test/suite/tests/draft6/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -235,5 +235,23 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds location-independent $id", + "schema": { + "$ref": "http://localhost:1234/draft6/detached-ref.json#/definitions/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft7/additionalItems.json b/src/test/suite/tests/draft7/additionalItems.json index cae72361c..2c7d15582 100644 --- a/src/test/suite/tests/draft7/additionalItems.json +++ b/src/test/suite/tests/draft7/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft7/maxLength.json b/src/test/suite/tests/draft7/maxLength.json index 748b4daaf..be60c5407 100644 --- a/src/test/suite/tests/draft7/maxLength.json +++ b/src/test/suite/tests/draft7/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft7/minLength.json b/src/test/suite/tests/draft7/minLength.json index 64db94805..23c68fe3f 100644 --- a/src/test/suite/tests/draft7/minLength.json +++ b/src/test/suite/tests/draft7/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft7/not.json b/src/test/suite/tests/draft7/not.json index 98de0eda8..b46c4ed05 100644 --- a/src/test/suite/tests/draft7/not.json +++ b/src/test/suite/tests/draft7/not.json @@ -93,19 +93,161 @@ ] }, { - "description": "not with boolean schema true", - "schema": {"not": true}, + "description": "forbid everything with empty schema", + "schema": { "not": {} }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", + "schema": { "not": true }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "allow everything with boolean schema false", + "schema": { "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true } ] }, { - "description": "not with boolean schema false", - "schema": {"not": false}, + "description": "double negation", + "schema": { "not": { "not": {} } }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft7/optional/format/ipv4.json b/src/test/suite/tests/draft7/optional/format/ipv4.json index 4706581f2..9680fe620 100644 --- a/src/test/suite/tests/draft7/optional/format/ipv4.json +++ b/src/test/suite/tests/draft7/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft7/id.json b/src/test/suite/tests/draft7/optional/id.json similarity index 100% rename from src/test/suite/tests/draft7/id.json rename to src/test/suite/tests/draft7/optional/id.json diff --git a/src/test/suite/tests/draft7/unknownKeyword.json b/src/test/suite/tests/draft7/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft7/unknownKeyword.json rename to src/test/suite/tests/draft7/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft7/ref.json b/src/test/suite/tests/draft7/ref.json index 82631726e..82e1e1672 100644 --- a/src/test/suite/tests/draft7/ref.json +++ b/src/test/suite/tests/draft7/ref.json @@ -445,6 +445,33 @@ } ] }, + { + "description": "Reference an anchor with a non-relative URI", + "schema": { + "$id": "https://example.com/schema-with-anchor", + "allOf": [{ + "$ref": "https://example.com/schema-with-anchor#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, { "description": "Location-independent identifier with base URI change in subschema", "schema": { diff --git a/src/test/suite/tests/draft7/refRemote.json b/src/test/suite/tests/draft7/refRemote.json index c2b200249..22185d678 100644 --- a/src/test/suite/tests/draft7/refRemote.json +++ b/src/test/suite/tests/draft7/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -235,5 +235,23 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds location-independent $id", + "schema": { + "$ref": "http://localhost:1234/draft7/detached-ref.json#/definitions/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] From 6de988a086314d8e2f64b38235aedcd2722abd64 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:52:22 +0800 Subject: [PATCH 48/53] Fix os line ending difference --- .../com/networknt/schema/OutputUnitTest.java | 181 +----------------- 1 file changed, 6 insertions(+), 175 deletions(-) diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java index f4a117c7c..840e3cdc8 100644 --- a/src/test/java/com/networknt/schema/OutputUnitTest.java +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -97,69 +97,8 @@ void annotationCollectionList() throws JsonProcessingException { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); }); - String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); - String expected = "{\r\n" - + " \"valid\" : false,\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/0\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\r\n" - + " \"instanceLocation\" : \"/foo\",\r\n" - + " \"errors\" : {\r\n" - + " \"required\" : \"required property 'unspecified-prop' not found\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" - + " \"errors\" : {\r\n" - + " \"const\" : \"must be a constant value 1\"\r\n" - + " },\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"title\" : \"foo-prop-title\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" - + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" - + " \"errors\" : {\r\n" - + " \"minimum\" : \"must have a minimum value of 10\"\r\n" - + " },\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"title\" : \"bar-prop-title\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" - + " \"instanceLocation\" : \"/foo\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"foo-prop\" ],\r\n" - + " \"title\" : \"foo-title\",\r\n" - + " \"additionalProperties\" : [ \"foo-prop\", \"other-prop\" ]\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" - + " \"instanceLocation\" : \"/bar\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"bar-prop\" ],\r\n" - + " \"title\" : \"bar-title\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" - + " \"instanceLocation\" : \"\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" - + " \"title\" : \"root\"\r\n" - + " }\r\n" - + " } ]\r\n" - + "}"; + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":false,\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be a constant value 1\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"}},{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"}}]}"; assertEquals(expected, output); } @@ -176,69 +115,8 @@ void annotationCollectionHierarchical() throws JsonProcessingException { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); }); - String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); - String expected = "{\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" - + " \"instanceLocation\" : \"\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" - + " \"title\" : \"root\"\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/0\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\r\n" - + " \"instanceLocation\" : \"/foo\",\r\n" - + " \"errors\" : {\r\n" - + " \"required\" : \"required property 'unspecified-prop' not found\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" - + " \"instanceLocation\" : \"/foo\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"foo-prop\" ],\r\n" - + " \"title\" : \"foo-title\",\r\n" - + " \"additionalProperties\" : [ \"foo-prop\", \"other-prop\" ]\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" - + " \"errors\" : {\r\n" - + " \"const\" : \"must be a constant value 1\"\r\n" - + " },\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"title\" : \"foo-prop-title\"\r\n" - + " }\r\n" - + " } ]\r\n" - + " }, {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" - + " \"instanceLocation\" : \"/bar\",\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"properties\" : [ \"bar-prop\" ],\r\n" - + " \"title\" : \"bar-title\"\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : false,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" - + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" - + " \"errors\" : {\r\n" - + " \"minimum\" : \"must have a minimum value of 10\"\r\n" - + " },\r\n" - + " \"droppedAnnotations\" : {\r\n" - + " \"title\" : \"bar-prop-title\"\r\n" - + " }\r\n" - + " } ]\r\n" - + " } ]\r\n" - + "}"; + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/0\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/0\",\"instanceLocation\":\"/foo\",\"errors\":{\"required\":\"required property 'unspecified-prop' not found\"}},{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"droppedAnnotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"other-prop\"]},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"errors\":{\"const\":\"must be a constant value 1\"},\"droppedAnnotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"errors\":{\"minimum\":\"must have a minimum value of 10\"},\"droppedAnnotations\":{\"title\":\"bar-prop-title\"}}]}]}"; assertEquals(expected, output); } @@ -255,55 +133,8 @@ void annotationCollectionHierarchical2() throws JsonProcessingException { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); }); - String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); - String expected = "{\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#\",\r\n" - + " \"instanceLocation\" : \"\",\r\n" - + " \"annotations\" : {\r\n" - + " \"properties\" : [ \"foo\", \"bar\" ],\r\n" - + " \"title\" : \"root\"\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\r\n" - + " \"instanceLocation\" : \"/foo\",\r\n" - + " \"annotations\" : {\r\n" - + " \"properties\" : [ \"foo-prop\" ],\r\n" - + " \"title\" : \"foo-title\",\r\n" - + " \"additionalProperties\" : [ \"foo-prop\", \"unspecified-prop\" ]\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\r\n" - + " \"instanceLocation\" : \"/foo/foo-prop\",\r\n" - + " \"annotations\" : {\r\n" - + " \"title\" : \"foo-prop-title\"\r\n" - + " }\r\n" - + " } ]\r\n" - + " }, {\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar\",\r\n" - + " \"instanceLocation\" : \"/bar\",\r\n" - + " \"annotations\" : {\r\n" - + " \"properties\" : [ \"bar-prop\" ],\r\n" - + " \"title\" : \"bar-title\"\r\n" - + " },\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"/properties/bar/$ref/properties/bar-prop\",\r\n" - + " \"schemaLocation\" : \"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\r\n" - + " \"instanceLocation\" : \"/bar/bar-prop\",\r\n" - + " \"annotations\" : {\r\n" - + " \"title\" : \"bar-prop-title\"\r\n" - + " }\r\n" - + " } ]\r\n" - + " } ]\r\n" - + "}"; + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"annotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"unspecified-prop\"]},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"annotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"annotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"annotations\":{\"title\":\"bar-prop-title\"}}]}]}"; assertEquals(expected, output); } } From a4d458e112cc0d8344ca99c1b1a14acc0e885c83 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 20:54:13 +0800 Subject: [PATCH 49/53] Fix os line ending difference --- .../schema/ContentSchemaValidatorTest.java | 36 ++----------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java index 86bf4017d..b0d16c68b 100644 --- a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java +++ b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java @@ -65,40 +65,8 @@ void annotationCollection() throws JsonProcessingException { executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); }); - String output = JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(outputUnit); - String expected = "{\r\n" - + " \"valid\" : true,\r\n" - + " \"details\" : [ {\r\n" - + " \"valid\" : true,\r\n" - + " \"evaluationPath\" : \"\",\r\n" - + " \"schemaLocation\" : \"#\",\r\n" - + " \"instanceLocation\" : \"\",\r\n" - + " \"annotations\" : {\r\n" - + " \"contentMediaType\" : \"application/jwt\",\r\n" - + " \"contentSchema\" : {\r\n" - + " \"type\" : \"array\",\r\n" - + " \"minItems\" : 2,\r\n" - + " \"prefixItems\" : [ {\r\n" - + " \"const\" : {\r\n" - + " \"typ\" : \"JWT\",\r\n" - + " \"alg\" : \"HS256\"\r\n" - + " }\r\n" - + " }, {\r\n" - + " \"type\" : \"object\",\r\n" - + " \"required\" : [ \"iss\", \"exp\" ],\r\n" - + " \"properties\" : {\r\n" - + " \"iss\" : {\r\n" - + " \"type\" : \"string\"\r\n" - + " },\r\n" - + " \"exp\" : {\r\n" - + " \"type\" : \"integer\"\r\n" - + " }\r\n" - + " }\r\n" - + " } ]\r\n" - + " }\r\n" - + " }\r\n" - + " } ]\r\n" - + "}"; + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":true,\"details\":[{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"annotations\":{\"contentMediaType\":\"application/jwt\",\"contentSchema\":{\"type\":\"array\",\"minItems\":2,\"prefixItems\":[{\"const\":{\"typ\":\"JWT\",\"alg\":\"HS256\"}},{\"type\":\"object\",\"required\":[\"iss\",\"exp\"],\"properties\":{\"iss\":{\"type\":\"string\"},\"exp\":{\"type\":\"integer\"}}}]}}}]}"; assertEquals(expected, output); } } From 1da19f961766f434999f81f600927d69c5aa2d13 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:05:09 +0800 Subject: [PATCH 50/53] Fix javadoc --- .../com/networknt/schema/annotation/JsonNodeAnnotation.java | 1 + .../com/networknt/schema/annotation/JsonNodeAnnotations.java | 4 ++-- .../java/com/networknt/schema/resource/MapSchemaLoader.java | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java index e717aa9a7..086bc35ee 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java @@ -82,6 +82,7 @@ public JsonNodePath getEvaluationPath() { /** * The attached value(s). * + * @param the value type * @return the value */ @SuppressWarnings("unchecked") diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java index 2c371a678..4e4f58278 100644 --- a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -36,14 +36,14 @@ public class JsonNodeAnnotations { /** * Stores the annotations. *

- * instancePath -> annotation + * instancePath to annotation */ private final Map> values = new LinkedHashMap<>(); /** * Gets the annotations. *

- * instancePath -> annotation + * instancePath to annotation * * @return the annotations */ diff --git a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java index 8fb04eabf..6d4b32f53 100644 --- a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java @@ -16,7 +16,7 @@ public class MapSchemaLoader implements SchemaLoader { /** * Sets the schema data by absolute IRI. * - * @param mappings + * @param mappings the mappings */ public MapSchemaLoader(Map mappings) { this(mappings::get); From d86c44ec90806b407f6e1f4b27be825d614a7355 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:27:14 +0800 Subject: [PATCH 51/53] Fix id handling --- .../java/com/networknt/schema/JsonSchema.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 33882fda3..acada7066 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -67,7 +67,13 @@ private static SchemaLocation resolve(SchemaLocation schemaLocation, JsonNode sc String id = validationContext.resolveSchemaId(schemaNode); if (id != null) { String resolve = id; - SchemaLocation result = schemaLocation.resolve(resolve); + 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)) { @@ -98,23 +104,23 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc 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 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; } From ea6569bdc55ec3ac5df1eeef1a2f51a5d1acf74a Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:03:02 +0800 Subject: [PATCH 52/53] Add tests for format output formatting --- .../schema/format/DateTimeValidator.java | 1 + .../com/networknt/schema/OutputUnitTest.java | 75 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 5d979893e..164f0efb9 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -65,6 +65,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!isLegalDateTime(node.textValue())) { if (assertionsEnabled) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .type("format") .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(node.textValue(), DATETIME).build()); diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java index 840e3cdc8..ad386a10b 100644 --- a/src/test/java/com/networknt/schema/OutputUnitTest.java +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -16,8 +16,13 @@ package com.networknt.schema; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; import com.fasterxml.jackson.core.JsonProcessingException; import com.networknt.schema.SpecVersion.VersionFlag; @@ -137,4 +142,74 @@ void annotationCollectionHierarchical2() throws JsonProcessingException { String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"annotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"unspecified-prop\"]},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1/properties/foo-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop\",\"instanceLocation\":\"/foo/foo-prop\",\"annotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"annotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"annotations\":{\"title\":\"bar-prop-title\"}}]}]}"; assertEquals(expected, output); } + + enum FormatInput { + DATE_TIME("date-time"), + DATE("date"), + TIME("time"), + DURATION("duration"), + EMAIL("email"), + IDN_EMAIL("idn-email"), + HOSTNAME("hostname"), + IDN_HOSTNAME("idn-hostname"), + IPV4("ipv4"), + IPV6("ipv6"), + URI("uri"), + URI_REFERENCE("uri-reference"), + IRI("iri"), + IRI_REFERENCE("iri-reference"), + UUID("uuid"), + JSON_POINTER("json-pointer"), + RELATIVE_JSON_POINTER("relative-json-pointer"), + REGEX("regex"); + + String format; + + FormatInput(String format) { + this.format = format; + } + } + + @ParameterizedTest + @EnumSource(FormatInput.class) + void formatAnnotation(FormatInput formatInput) { + String formatSchema = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"format\": \""+formatInput.format+"\"\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(formatSchema, config); + OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + assertTrue(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertEquals(formatInput.format, details.getAnnotations().get("format")); + } + + @ParameterizedTest + @EnumSource(FormatInput.class) + void formatAssertion(FormatInput formatInput) { + String formatSchema = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"format\": \""+formatInput.format+"\"\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(formatSchema, config); + OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + assertFalse(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertEquals(formatInput.format, details.getDroppedAnnotations().get("format")); + assertNotNull(details.getErrors().get("format")); + } + } From d008b9b560b1498b3d3d5836423117e51a5c0981 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 1 Feb 2024 09:45:30 +0800 Subject: [PATCH 53/53] Add test for type union --- .../networknt/schema/UnionTypeValidator.java | 1 + .../com/networknt/schema/OutputUnitTest.java | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index 630e34d30..c04626a08 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -90,6 +90,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!valid) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .type("type") .locale(executionContext.getExecutionConfig().getLocale()) .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(nodeType.toString(), error) .build()); diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java index ad386a10b..cfa0bdf4d 100644 --- a/src/test/java/com/networknt/schema/OutputUnitTest.java +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -212,4 +212,22 @@ void formatAssertion(FormatInput formatInput) { assertNotNull(details.getErrors().get("format")); } + @Test + void typeUnion() { + String typeSchema = "{\r\n" + + " \"type\": [\"string\",\"array\"]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(typeSchema, config); + OutputUnit outputUnit = schema.validate("1", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + assertFalse(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertNotNull(details.getErrors().get("type")); + } + }