diff --git a/src/main/java/com/networknt/schema/AbsoluteIri.java b/src/main/java/com/networknt/schema/AbsoluteIri.java index ebfdb419d..858afa51d 100644 --- a/src/main/java/com/networknt/schema/AbsoluteIri.java +++ b/src/main/java/com/networknt/schema/AbsoluteIri.java @@ -105,14 +105,23 @@ public static String resolve(String parent, String iri) { } else { scheme = scheme + 3; } - int slash = parent.lastIndexOf('/'); - if (slash != -1 && slash > scheme) { - base = parent.substring(0, slash); + base = parent(base, scheme); + while (iri.startsWith("../")) { + base = parent(base, scheme); + iri = iri.substring(3); } return base + "/" + iri; } } } + + protected static String parent(String iri, int scheme) { + int slash = iri.lastIndexOf('/'); + if (slash != -1 && slash > scheme) { + return iri.substring(0, slash); + } + return iri; + } /** * Returns the scheme and authority components of the IRI. diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 30074a548..415e8e958 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -69,7 +69,7 @@ public Set validate(ExecutionContext executionContext, JsonNo final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); final JsonNode $ref = allOfEntry.get("$ref"); if (null != $ref) { - final ValidationContext.DiscriminatorContext currentDiscriminatorContext = this.validationContext + final DiscriminatorContext currentDiscriminatorContext = executionContext .getCurrentDiscriminatorContext(); if (null != currentDiscriminatorContext) { final ObjectNode discriminator = currentDiscriminatorContext diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 99ffb905a..f3828cc1d 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -30,7 +30,7 @@ public class AnyOfValidator extends BaseJsonValidator { private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation"; private final List schemas = new ArrayList<>(); - private final ValidationContext.DiscriminatorContext discriminatorContext; + private final DiscriminatorContext discriminatorContext; public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); @@ -42,7 +42,7 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath } if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - this.discriminatorContext = new ValidationContext.DiscriminatorContext(); + this.discriminatorContext = new DiscriminatorContext(); } else { this.discriminatorContext = null; } @@ -57,7 +57,7 @@ public Set validate(ExecutionContext executionContext, JsonNo ValidatorState state = executionContext.getValidatorState(); if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - this.validationContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation); + executionContext.enterDiscriminatorContext(this.discriminatorContext, instanceLocation); } boolean initialHasMatchedNode = state.hasMatchedNode(); @@ -148,7 +148,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } } finally { if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - this.validationContext.leaveDiscriminatorContextImmediately(instanceLocation); + executionContext.leaveDiscriminatorContextImmediately(instanceLocation); } Scope parentScope = collectorContext.exitDynamicScope(); diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 6de3bf544..98596cbcb 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.networknt.schema.ValidationContext.DiscriminatorContext; import com.networknt.schema.i18n.DefaultMessageSource; import org.slf4j.Logger; @@ -69,6 +68,20 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP : PathType.DEFAULT; } + /** + * Copy constructor. + * + * @param copy to copy from + */ + protected BaseJsonValidator(BaseJsonValidator copy) { + super(copy); + this.suppressSubSchemaRetrieval = copy.suppressSubSchemaRetrieval; + this.applyDefaultsStrategy = copy.applyDefaultsStrategy; + this.pathType = copy.pathType; + this.schemaNode = copy.schemaNode; + this.validationContext = copy.validationContext; + } + private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) { final JsonNode node = schemaNode.get("id"); @@ -112,7 +125,7 @@ protected static void debug(Logger logger, JsonNode node, JsonNode rootNode, Jso * @param discriminatorPropertyValue the value of the discriminator/propertyName field * @param jsonSchema the {@link JsonSchema} to check */ - protected static void checkDiscriminatorMatch(final ValidationContext.DiscriminatorContext currentDiscriminatorContext, + protected static void checkDiscriminatorMatch(final DiscriminatorContext currentDiscriminatorContext, final ObjectNode discriminator, final String discriminatorPropertyValue, final JsonSchema jsonSchema) { @@ -249,6 +262,13 @@ public JsonSchema getParentSchema() { return this.parentSchema; } + public JsonSchema getEvaluationParentSchema() { + if (this.evaluationParentSchema != null) { + return this.evaluationParentSchema; + } + return getParentSchema(); + } + protected JsonSchema fetchSubSchemaNode(ValidationContext validationContext) { return this.suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(this.schemaNode, validationContext); } diff --git a/src/main/java/com/networknt/schema/CachedSupplier.java b/src/main/java/com/networknt/schema/CachedSupplier.java new file mode 100644 index 000000000..109f36cbf --- /dev/null +++ b/src/main/java/com/networknt/schema/CachedSupplier.java @@ -0,0 +1,41 @@ +/* + * 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.function.Supplier; + +/** + * Supplier that caches the output. + * + * @param the type cached + */ +public class CachedSupplier implements Supplier { + private final Supplier delegate; + private T cache = null; + + public CachedSupplier(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public T get() { + if (cache == null) { + cache = delegate.get(); + } + return cache; + } + +} diff --git a/src/main/java/com/networknt/schema/DiscriminatorContext.java b/src/main/java/com/networknt/schema/DiscriminatorContext.java new file mode 100644 index 000000000..5ffeac39c --- /dev/null +++ b/src/main/java/com/networknt/schema/DiscriminatorContext.java @@ -0,0 +1,41 @@ +package com.networknt.schema; + +import java.util.HashMap; +import java.util.Map; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class DiscriminatorContext { + private final Map discriminators = new HashMap<>(); + + private boolean discriminatorMatchFound = false; + + public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) { + this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator); + } + + public ObjectNode getDiscriminatorForPath(final SchemaLocation schemaLocation) { + return this.discriminators.get("#" + schemaLocation.getFragment().toString()); + } + + public ObjectNode getDiscriminatorForPath(final String schemaLocation) { + return this.discriminators.get(schemaLocation); + } + + public void markMatch() { + this.discriminatorMatchFound = true; + } + + public boolean isDiscriminatorMatchFound() { + return this.discriminatorMatchFound; + } + + /** + * Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure + * + * @return true in case there are discriminator candidates + */ + public boolean isActive() { + return !this.discriminators.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index 459b63081..adbde1a55 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -17,11 +17,13 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.DecimalNode; import com.fasterxml.jackson.databind.node.NullNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.math.BigDecimal; import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -45,7 +47,10 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, for (JsonNode n : schemaNode) { if (n.isNumber()) { // convert to DecimalNode for number comparison - nodes.add(DecimalNode.valueOf(n.decimalValue())); + nodes.add(processNumberNode(n)); + } else if (n.isArray()) { + ArrayNode a = processArrayNode((ArrayNode) n); + nodes.add(a); } else { nodes.add(n); } @@ -65,7 +70,6 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, sb.append("null"); } } - // sb.append(']'); error = sb.toString(); @@ -78,7 +82,11 @@ public EnumValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - if (node.isNumber()) node = DecimalNode.valueOf(node.decimalValue()); + if (node.isNumber()) { + node = processNumberNode(node); + } else if (node.isArray()) { + node = processArrayNode((ArrayNode) node); + } if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { return Collections.singleton(message().instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build()); @@ -105,4 +113,50 @@ private boolean isTypeLooseContainsInEnum(JsonNode node) { return false; } + /** + * Processes the number and ensures trailing zeros are stripped. + * + * @param n the node + * @return the node + */ + protected JsonNode processNumberNode(JsonNode n) { + return DecimalNode.valueOf(new BigDecimal(n.decimalValue().toPlainString())); + } + + /** + * Processes the array and ensures that numbers within have trailing zeroes stripped. + * + * @param node the node + * @return the node + */ + protected ArrayNode processArrayNode(ArrayNode node) { + if (!hasNumber(node)) { + return node; + } + ArrayNode a = (ArrayNode) node.deepCopy(); + for (int x = 0; x < a.size(); x++) { + JsonNode v = a.get(x); + if (v.isNumber()) { + v = processNumberNode(v); + a.set(x, v); + } + } + return a; + } + + /** + * Determines if the array node contains a number. + * + * @param node the node + * @return the node + */ + protected boolean hasNumber(ArrayNode node) { + for (int x = 0; x < node.size(); x++) { + JsonNode v = node.get(x); + if (v.isNumber()) { + return true; + } + } + return false; + } } diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index 0e14dd991..c3129eb03 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 java.util.Stack; + /** * Stores the execution context for the validation run. */ @@ -23,6 +25,7 @@ public class ExecutionContext { private ExecutionConfig executionConfig; private CollectorContext collectorContext; private ValidatorState validatorState = null; + private Stack discriminatorContexts = new Stack<>(); /** * Creates an execution context. @@ -113,4 +116,19 @@ public ValidatorState getValidatorState() { public void setValidatorState(ValidatorState validatorState) { this.validatorState = validatorState; } + + public DiscriminatorContext getCurrentDiscriminatorContext() { + if (!this.discriminatorContexts.empty()) { + return this.discriminatorContexts.peek(); + } + return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf + } + + public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) { + this.discriminatorContexts.push(ctx); + } + + public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") JsonNodePath instanceLocation) { + this.discriminatorContexts.pop(); + } } diff --git a/src/main/java/com/networknt/schema/JsonMetaSchema.java b/src/main/java/com/networknt/schema/JsonMetaSchema.java index 71a80d037..7c688a1aa 100644 --- a/src/main/java/com/networknt/schema/JsonMetaSchema.java +++ b/src/main/java/com/networknt/schema/JsonMetaSchema.java @@ -226,6 +226,14 @@ public String readId(JsonNode schemaNode) { return readText(schemaNode, this.idKeyword); } + public String readAnchor(JsonNode schemaNode) { + boolean supportsAnchor = this.keywords.containsKey("$anchor"); + if (supportsAnchor) { + return readText(schemaNode, "$anchor"); + } + return null; + } + public JsonNode getNodeByFragmentRef(String ref, JsonNode node) { boolean supportsAnchor = this.keywords.containsKey("$anchor"); String refName = supportsAnchor ? ref.substring(1) : ref; diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index b142ac9aa..bdf2d4763 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -20,14 +20,11 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.SpecVersion.VersionFlag; -import com.networknt.schema.ValidationContext.DiscriminatorContext; -import com.networknt.schema.utils.StringUtils; import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; import java.io.UnsupportedEncodingException; import java.net.URI; -import java.net.URISyntaxException; import java.net.URLDecoder; import java.util.*; @@ -57,12 +54,14 @@ public class JsonSchema extends BaseJsonValidator { * 'id' would still be able to specify an absolute uri. */ private URI currentUri; - private boolean hasId = false; private JsonValidator requiredValidator = null; private TypeValidator typeValidator; WalkListenerRunner keywordWalkListenerRunner = null; + private final String id; + private final String anchor; + static JsonSchema from(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { return new JsonSchema(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parent, suppressSubSchemaRetrieval); } @@ -74,22 +73,90 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc this.validationContext = validationContext; this.metaSchema = validationContext.getMetaSchema(); this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode); - if (uriRefersToSubschema(currentUri, schemaLocation)) { - updateThisAsSubschema(currentUri); + initializeConfig(); + this.id = validationContext.resolveSchemaId(this.schemaNode); + this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); + if (this.id != null) { + this.validationContext.getSchemaResources() + .putIfAbsent(this.currentUri != null ? this.currentUri.toString() : this.id, this); + } + if (this.anchor != null) { + this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString() + "#" + anchor, this); } + getValidators(); + } + + private void initializeConfig() { if (validationContext.getConfig() != null) { - this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap()); - if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator"); - if (null != discriminator && null != validationContext.getCurrentDiscriminatorContext()) { - validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaLocation, discriminator); - } - } + this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner( + this.validationContext.getConfig().getKeywordWalkListenersMap()); } } - public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { - return getValidationContext().newSchema(schemaLocation, evaluationPath, schemaNode, this); + /** + * Copy constructor. + * + * @param copy to copy from + */ + protected JsonSchema(JsonSchema copy) { + super(copy); + this.validators = copy.validators; + this.metaSchema = copy.metaSchema; + this.validatorsLoaded = copy.validatorsLoaded; + this.dynamicAnchor = copy.dynamicAnchor; + this.currentUri = copy.currentUri; + this.requiredValidator = copy.requiredValidator; + this.typeValidator = copy.typeValidator; + this.keywordWalkListenerRunner = copy.keywordWalkListenerRunner; + this.id = copy.id; + this.anchor = copy.anchor; + } + + /** + * Creates a schema using the current one as a template with the parent as the + * ref. + *

+ * This is typically used if this schema is a schema resource that can be + * pointed to by various references. + * + * @param refEvaluationParentSchema the parent ref + * @param refEvaluationPath the ref evaluation path + * @return the schema + */ + public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) { + JsonSchema copy = new JsonSchema(this); + copy.validationContext = new ValidationContext(copy.validationContext.getURIFactory(), + copy.getValidationContext().getURNFactory(), copy.getValidationContext().getMetaSchema(), + copy.getValidationContext().getJsonSchemaFactory(), + refEvaluationParentSchema.validationContext.getConfig(), + copy.getValidationContext().getSchemaReferences(), copy.getValidationContext().getSchemaResources()); + copy.evaluationPath = refEvaluationPath; + copy.evaluationParentSchema = refEvaluationParentSchema; + // Validator state is reset due to the changes in evaluation path + copy.validatorsLoaded = false; + copy.requiredValidator = null; + copy.typeValidator = null; + copy.validators = null; + copy.initializeConfig(); + return copy; + } + + public JsonSchema withConfig(SchemaValidatorsConfig config) { + if (!this.getValidationContext().getConfig().equals(config)) { + JsonSchema copy = new JsonSchema(this); + copy.validationContext = new ValidationContext(copy.validationContext.getURIFactory(), + copy.getValidationContext().getURNFactory(), copy.getValidationContext().getMetaSchema(), + copy.getValidationContext().getJsonSchemaFactory(), config, + copy.getValidationContext().getSchemaReferences(), + copy.getValidationContext().getSchemaResources()); + copy.validatorsLoaded = false; + copy.requiredValidator = null; + copy.typeValidator = null; + copy.validators = null; + copy.initializeConfig(); + return copy; + } + return this; } ValidationContext getValidationContext() { @@ -120,36 +187,7 @@ private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { } private static boolean isUriFragmentWithNoContext(URI currentUri, String id) { - return id.startsWith("#") && currentUri == null; - } - - private static boolean uriRefersToSubschema(URI originalUri, SchemaLocation schemaLocation) { - return originalUri != null - && StringUtils.isNotBlank(originalUri.getRawFragment()) // Original currentUri parameter has a fragment, so it refers to a subschema - && (schemaLocation.getFragment().getNameCount() == 0); // We aren't already in a subschema - } - - /** - * Creates a new parent schema from the current state and updates this object to refer to the subschema instead. - */ - private void updateThisAsSubschema(URI originalUri) { - String fragment = "#" + originalUri.getFragment(); - JsonNode fragmentSchemaNode = getRefSchemaNode(fragment); - if (fragmentSchemaNode == null) { - throw new JsonSchemaException("Fragment " + fragment + " cannot be resolved"); - } - // We need to strip the fragment off of the new parent schema's currentUri, so that its constructor - // won't also end up in this method and get stuck in an infinite recursive loop. - URI currentUriWithoutFragment; - try { - currentUriWithoutFragment = new URI(this.currentUri.getScheme(), this.currentUri.getSchemeSpecificPart(), null); - } catch (URISyntaxException ex) { - throw new JsonSchemaException("Unable to create URI without fragment from " + this.currentUri + ": " + ex.getMessage()); - } - this.parentSchema = new JsonSchema(this.validationContext, SchemaLocation.of(currentUriWithoutFragment.toString()), this.evaluationPath, currentUriWithoutFragment, this.schemaNode, this.parentSchema, super.suppressSubSchemaRetrieval); // TODO: Should this be delegated to the factory? - this.schemaLocation = SchemaLocation.of(originalUri.toString()); - this.schemaNode = fragmentSchemaNode; - this.currentUri = combineCurrentUriWithIds(this.currentUri, fragmentSchemaNode); + return id.startsWith("#") && (currentUri == null || currentUri.toString().startsWith("#")); } public URI getCurrentUri() { @@ -163,12 +201,16 @@ public URI getCurrentUri() { * @return JsonNode */ public JsonNode getRefSchemaNode(String ref) { - JsonSchema schema = findAncestor(); + JsonSchema schema = findSchemaResourceRoot(); JsonNode node = schema.getSchemaNode(); String jsonPointer = ref; + if (schema.getId() != null && ref.startsWith(schema.getId())) { + String refValue = ref.substring(schema.getId().length()); + jsonPointer = refValue; + } if (jsonPointer.startsWith("#/")) { - jsonPointer = ref.substring(1); + jsonPointer = jsonPointer.substring(1); } if (jsonPointer.startsWith("/")) { @@ -192,16 +234,69 @@ public JsonNode getRefSchemaNode(String ref) { return node; } - // This represents the lexical scope - JsonSchema findLexicalRoot() { + public JsonSchema findLexicalRoot() { JsonSchema ancestor = this; - while (!ancestor.hasId) { + while (ancestor.getId() == null) { if (null == ancestor.getParentSchema()) break; ancestor = ancestor.getParentSchema(); } return ancestor; } + /** + * Finds the root of the schema resource. + *

+ * This is either the schema document root or the subschema resource root. + * + * @return the root of the schema + */ + public JsonSchema findSchemaResourceRoot() { + JsonSchema ancestor = this; + while (!ancestor.isSchemaResourceRoot()) { + ancestor = ancestor.getParentSchema(); + } + return ancestor; + } + + /** + * Determines if this schema resource is a schema resource root. + *

+ * This is either the schema document root or the subschema resource root. + * + * @return if this schema is a schema resource root + */ + public boolean isSchemaResourceRoot() { + if (getId() != null) { + return true; + } + if (getParentSchema() == null) { + return true; + } + // The schema should not cross + if (getCurrentUri() != null && getParentSchema().getCurrentUri() == null) { + return true; + } + if (getCurrentUri() == null && getParentSchema().getCurrentUri() != null) { + return true; + } + if (getCurrentUri() != null && getParentSchema().getCurrentUri() != null) { + if (!Objects.equals(getCurrentUri().getScheme(), getParentSchema().getCurrentUri().getScheme())) { + return true; + } + if (!Objects.equals(getCurrentUri().getHost(), getParentSchema().getCurrentUri().getHost())) { + return true; + } + if (!Objects.equals(getCurrentUri().getPath(), getParentSchema().getCurrentUri().getPath())) { + return true; + } + } + return false; + } + + public String getId() { + return this.id; + } + public JsonSchema findAncestor() { JsonSchema ancestor = this; if (this.getParentSchema() != null) { @@ -217,7 +312,7 @@ private JsonNode handleNullNode(String ref, JsonSchema schema) { } return null; } - + /** * Please note that the key in {@link #validators} map is the evaluation path. */ @@ -236,9 +331,6 @@ private List read(JsonNode schemaNode) { validators.add(validator); } } else { - - this.hasId = schemaNode.has(this.validationContext.getMetaSchema().getIdKeyword()); - JsonValidator refValidator = null; Iterator pnames = schemaNode.fieldNames(); @@ -326,6 +418,14 @@ private long activeDialect() { @Override public Set validate(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) { + if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator"); + if (null != discriminator && null != executionContext.getCurrentDiscriminatorContext()) { + executionContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaLocation, + discriminator); + } + } + SchemaValidatorsConfig config = this.validationContext.getConfig(); Set errors = null; // Get the collector context. @@ -364,7 +464,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (config.isOpenAPI3StyleDiscriminators()) { ObjectNode discriminator = (ObjectNode) this.schemaNode.get("discriminator"); if (null != discriminator) { - final DiscriminatorContext discriminatorContext = this.validationContext + final DiscriminatorContext discriminatorContext = executionContext .getCurrentDiscriminatorContext(); if (null != discriminatorContext) { final ObjectNode discriminatorToUse; diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index b43b190c2..03d7cf5b9 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -212,7 +212,7 @@ public JsonSchemaFactory build() { private final URNFactory urnFactory; private final Map jsonMetaSchemas; private final Map uriMap; - private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap(); + private final ConcurrentMap uriSchemaCache = new ConcurrentHashMap<>(); private final boolean enableUriSchemaCache; @@ -325,22 +325,25 @@ public static Builder builder(final JsonSchemaFactory blueprint) { } protected JsonSchema newJsonSchema(final URI schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { - final ValidationContext validationContext = createValidationContext(schemaNode); - validationContext.setConfig(config); + final ValidationContext validationContext = createValidationContext(schemaNode, config); return doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), new JsonNodePath(validationContext.getConfig().getPathType()), schemaUri, schemaNode, null, false); } - - public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { + + public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema) { return doCreate(validationContext, - null == schemaLocation ? getSchemaLocation(null, schemaNode, validationContext) : schemaLocation, - evaluationPath, parentSchema.getCurrentUri(), schemaNode, parentSchema, false); + null == schemaLocation ? getSchemaLocation(currentUri, schemaNode, validationContext) : schemaLocation, + evaluationPath, currentUri, schemaNode, parentSchema, false); + } + + public JsonSchema create(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { + return create(validationContext, schemaLocation, evaluationPath, parentSchema.getCurrentUri(), schemaNode, parentSchema); } private JsonSchema doCreate(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema, boolean suppressSubSchemaRetrieval) { return JsonSchema.from(validationContext, schemaLocation, evaluationPath, currentUri, schemaNode, parentSchema, suppressSubSchemaRetrieval); } - + /** * Gets the schema location from the $id or retrieval uri. * @@ -358,9 +361,9 @@ protected SchemaLocation getSchemaLocation(URI schemaRetrievalUri, JsonNode sche return schemaLocation != null ? SchemaLocation.of(schemaLocation) : SchemaLocation.DOCUMENT; } - protected ValidationContext createValidationContext(final JsonNode schemaNode) { + protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); - return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, null); + return new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); } private JsonMetaSchema findMetaSchemaForSchema(final JsonNode schemaNode) { @@ -421,68 +424,60 @@ public JsonSchema getSchema(final InputStream schemaStream) { } public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig config) { + final URITranslator uriTranslator = null == config ? getUriTranslator() + : config.getUriTranslator().with(getUriTranslator()); + + final URI mappedUri; try { - InputStream inputStream = null; - final URITranslator uriTranslator = null == config ? getUriTranslator() : config.getUriTranslator().with(getUriTranslator()); - - final URI mappedUri; - try { - mappedUri = this.uriFactory.create(uriTranslator.translate(schemaUri).toString()); - } catch (IllegalArgumentException e) { - logger.error("Failed to create URI.", e); - throw new JsonSchemaException(e); - } + mappedUri = this.uriFactory.create(uriTranslator.translate(schemaUri).toString()); + } catch (IllegalArgumentException e) { + logger.error("Failed to create URI.", e); + throw new JsonSchemaException(e); + } - if (enableUriSchemaCache && uriSchemaCache.containsKey(mappedUri)) { - JsonSchema cachedUriSchema = uriSchemaCache.get(mappedUri); - // 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. - cachedUriSchema.getValidationContext().setConfig(config); - return cachedUriSchema; + if (enableUriSchemaCache) { + JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(mappedUri, key -> { + return getMappedSchema(schemaUri, config, mappedUri); + }); + return cachedUriSchema.withConfig(config); + } + return getMappedSchema(schemaUri, config, mappedUri); + } + + protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig config, final URI mappedUri) { + try (InputStream inputStream = this.uriFetcher.fetch(mappedUri)) { + final JsonNode schemaNode; + if (isYaml(mappedUri)) { + schemaNode = yamlMapper.readTree(inputStream); + } else { + schemaNode = jsonMapper.readTree(inputStream); } - try { - inputStream = this.uriFetcher.fetch(mappedUri); - - final JsonNode schemaNode; - if (isYaml(mappedUri)) { - schemaNode = yamlMapper.readTree(inputStream); - } else { - schemaNode = jsonMapper.readTree(inputStream); - } - - final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); - JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); - JsonSchema jsonSchema; - if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri)) { - String schemaLocationValue = schemaUri.toString(); - if(!schemaLocationValue.contains("#")) { - schemaLocationValue = schemaLocationValue + "#"; - } - SchemaLocation schemaLocation = SchemaLocation.of(schemaLocationValue); - ValidationContext validationContext = new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode); + JsonNodePath evaluationPath = new JsonNodePath(config.getPathType()); + JsonSchema jsonSchema; + SchemaLocation schemaLocation = SchemaLocation.of(schemaUri.toString()); + if (idMatchesSourceUri(jsonMetaSchema, schemaNode, schemaUri) || schemaUri.getFragment() == null + || "".equals(schemaUri.getFragment())) { + ValidationContext validationContext = new ValidationContext(this.uriFactory, this.urnFactory, jsonMetaSchema, this, config); + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); + } else { + // Subschema + final ValidationContext validationContext = createValidationContext(schemaNode, config); + URI documentUri = "".equals(schemaUri.getSchemeSpecificPart()) ? new URI(schemaUri.getScheme(), schemaUri.getUserInfo(), schemaUri.getHost(), schemaUri.getPort(), schemaUri.getPath(), schemaUri.getQuery(), null) : new URI(schemaUri.getScheme(), schemaUri.getSchemeSpecificPart(), null); + SchemaLocation documentLocation = new SchemaLocation(schemaLocation.getAbsoluteIri()); + JsonSchema document = doCreate(validationContext, documentLocation, evaluationPath, documentUri, schemaNode, null, false); + JsonNode subSchemaNode = document.getRefSchemaNode(schemaLocation.getFragment().toString()); + if (subSchemaNode != null) { + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, subSchemaNode, document, false); } else { - final ValidationContext validationContext = createValidationContext(schemaNode); - validationContext.setConfig(config); - jsonSchema = doCreate(validationContext, SchemaLocation.DOCUMENT, evaluationPath, mappedUri, - schemaNode, null, false); - } - - if (enableUriSchemaCache) { - uriSchemaCache.put(mappedUri, jsonSchema); - } - - return jsonSchema; - } finally { - if (inputStream != null) { - inputStream.close(); + throw new JsonSchemaException("Unable to find subschema"); } } - } catch (IOException ioe) { - logger.error("Failed to load json schema!", ioe); - throw new JsonSchemaException(ioe); + return jsonSchema; + } catch (IOException | URISyntaxException e) { + logger.error("Failed to load json schema from {}", schemaUri, e); + throw new JsonSchemaException(e); } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaRef.java b/src/main/java/com/networknt/schema/JsonSchemaRef.java index 77b3d2114..c4d4cf4ca 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaRef.java +++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java @@ -15,36 +15,20 @@ */ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.Set; +import java.util.function.Supplier; /** * Use this object instead a JsonSchema for references. - *

- * This reference may be empty (if the reference is being parsed) or with data (after the reference has been parsed), - * helping to prevent recursive reference to cause an infinite loop. */ - public class JsonSchemaRef { - private final JsonSchema schema; - - public JsonSchemaRef(JsonSchema schema) { - this.schema = schema; - } + private final Supplier schemaSupplier; - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation) { - return schema.validate(executionContext, node, rootNode, instanceLocation); + public JsonSchemaRef(Supplier schema) { + this.schemaSupplier = schema; } public JsonSchema getSchema() { - return schema; - } - - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - return schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + return this.schemaSupplier.get(); } } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index 45f8cfc8d..ef1b8d9f5 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Collections; +import java.util.Iterator; +import java.util.Map.Entry; import java.util.Set; /** @@ -27,8 +29,22 @@ public class NonValidationKeyword extends AbstractKeyword { private static final class Validator extends AbstractJsonValidator { - public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword) { + public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { super(schemaLocation, evaluationPath, keyword); + String id = validationContext.resolveSchemaId(schemaNode); + String anchor = validationContext.getMetaSchema().readAnchor(schemaNode); + if (id != null || anchor != null) { + // Used to register schema resources with $id + validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); + } + if ("$defs".equals(keyword.getValue()) || "definitions".equals(keyword.getValue())) { + for (Iterator> field = schemaNode.fields(); field.hasNext(); ) { + Entry property = field.next(); + validationContext.newSchema(schemaLocation.append(property.getKey()), + evaluationPath.append(property.getKey()), property.getValue(), parentSchema); + } + } } @Override @@ -44,6 +60,6 @@ public NonValidationKeyword(String keyword) { @Override public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { - return new Validator(schemaLocation, evaluationPath, this); + return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this); } } diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index ce0d4eba1..16714be22 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -26,6 +26,8 @@ public class RecursiveRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class); + private Map schemas = new HashMap<>(); + public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext); @@ -51,11 +53,10 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonSchema schema = collectorContext.getOutermostSchema(); if (null != schema) { - // 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. - schema.getValidationContext().setConfig(getParentSchema().getValidationContext().getConfig()); - errors = schema.validate(executionContext, node, rootNode, instanceLocation); + JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> { + return schema.fromRef(getParentSchema(), getEvaluationPath()); + }); + errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } } finally { Scope scope = collectorContext.exitDynamicScope(); @@ -79,11 +80,10 @@ public Set walk(ExecutionContext executionContext, JsonNode n JsonSchema schema = collectorContext.getOutermostSchema(); if (null != schema) { - // 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. - schema.getValidationContext().setConfig(getParentSchema().getValidationContext().getConfig()); - errors = schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> { + return schema.fromRef(getParentSchema(), getEvaluationPath()); + }); + errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } } finally { Scope scope = collectorContext.exitDynamicScope(); diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 5f0649a09..1e5c74ee5 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -19,35 +19,26 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.uri.URIFactory; -import com.networknt.schema.uri.URNURIFactory; import com.networknt.schema.urn.URNFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.URI; -import java.util.*; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); protected JsonSchemaRef schema; - private JsonSchema parentSchema; - private static final String REF_CURRENT = "#"; - private static final String URN_SCHEME = URNURIFactory.SCHEME; public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); String refValue = schemaNode.asText(); - this.parentSchema = parentSchema; this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); - if (this.schema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) - .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .instanceLocation(schemaLocation.getFragment()).evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); - throw new JsonSchemaException(validationMessage); - } } static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, @@ -55,7 +46,6 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; - JsonSchema parent = parentSchema; if (!refValue.startsWith(REF_CURRENT)) { // This will be the uri extracted from the refValue (this may be a relative or absolute uri). final String refUri; @@ -68,7 +58,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // This will determine the correct absolute uri for the refUri. This decision will take into // account the current uri of the parent schema. - URI schemaUri = determineSchemaUri(validationContext.getURIFactory(), parent, refUri); + URI schemaUri = determineSchemaUri(validationContext.getURIFactory(), parentSchema, refUri); if (schemaUri == null) { // the URNFactory is optional if (validationContext.getURNFactory() == null) { @@ -79,63 +69,141 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val if (schemaUri == null) { return null; } - } else if (URN_SCHEME.equals(schemaUri.getScheme())) { - // Try to resolve URN schema as a JsonSchemaRef to some sub-schema of the parent - JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, schemaUri.toString(), refValueOriginal, evaluationPath); - if (ref != null) { - return ref; - } } + URI schemaUriFinal = schemaUri; // This should retrieve schemas regardless of the protocol that is in the uri. - parent = validationContext.getJsonSchemaFactory().getSchema(schemaUri, validationContext.getConfig()); - - if (index < 0) { - return new JsonSchemaRef(parent.findAncestor()); + return new JsonSchemaRef(new CachedSupplier<>(() -> { + JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); + if (schemaResource == null) { + schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()); + if (schemaResource != null) { + if (!schemaResource.getValidationContext().getSchemaResources().isEmpty()) { + validationContext.getSchemaResources() + .putAll(schemaResource.getValidationContext().getSchemaResources()); + } + if (!schemaResource.getValidationContext().getSchemaReferences().isEmpty()) { + validationContext.getSchemaReferences() + .putAll(schemaResource.getValidationContext().getSchemaReferences()); + } + } + } + if (index < 0) { + if (schemaResource == null) { + return null; + } + return schemaResource.fromRef(parentSchema, evaluationPath); + } else { + String newRefValue = refValue.substring(index); + schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, + evaluationPath); + if (schemaResource == null) { + return null; + } + return schemaResource.fromRef(parentSchema, evaluationPath); + } + })); + + } else if (SchemaLocation.Fragment.isAnchorFragment(refValue)) { + // $ref prevents a sibling $id from changing the base uri + JsonSchema base = parentSchema; + if (parentSchema.getId() != null && parentSchema.parentSchema != null) { + base = parentSchema.parentSchema; + } + if (base.getCurrentUri() != null) { + String absoluteIri = SchemaLocation.resolve(base.getSchemaLocation(), refValue); + // Schema resource needs to update the parent and evaluation path + return new JsonSchemaRef(new CachedSupplier<>(() -> { + JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); + if (schemaResource == null) { + schemaResource = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); + } + if (schemaResource == null) { + return null; + } + return schemaResource.fromRef(parentSchema, evaluationPath); + })); } - refValue = refValue.substring(index); } if (refValue.equals(REF_CURRENT)) { - return new JsonSchemaRef(parent.findAncestor()); + return new JsonSchemaRef(new CachedSupplier<>( + () -> parentSchema.findSchemaResourceRoot().fromRef(parentSchema, evaluationPath))); } - return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); + return new JsonSchemaRef(new CachedSupplier<>( + () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath))); } - private static JsonSchemaRef getJsonSchemaRef(JsonSchema parent, + private static JsonSchema getJsonSchema(JsonSchema parent, ValidationContext validationContext, String refValue, String refValueOriginal, JsonNodePath evaluationPath) { JsonNode node = parent.getRefSchemaNode(refValue); if (node != null) { - JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal); - if (ref == null) { - SchemaLocation path = null; - if (refValue.startsWith(REF_CURRENT)) { - // relative - path = parent.schemaLocation; - JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); - String[] parts = refValue.split("/"); - for (int x = 1; x < parts.length; x++) { - fragment = fragment.append(parts[x]); + return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { + return getJsonSchema(node, parent, validationContext, refValue, evaluationPath); + }); + } + return null; + } + + private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, + ValidationContext validationContext, + String refValue, + JsonNodePath evaluationPath) { + if (node != null) { + SchemaLocation path = null; + JsonSchema currentParent = parent; + URI currentUri = parent.getCurrentUri(); + if (refValue.startsWith(REF_CURRENT)) { + // relative to document + path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), + new JsonNodePath(PathType.JSON_POINTER)); + // Attempt to get subschema node + String[] refParts = refValue.split("/"); + if (refParts.length > 3) { + String[] subschemaParts = Arrays.copyOf(refParts, refParts.length - 2); + JsonNode subschemaNode = parent.getRefSchemaNode(String.join("/", subschemaParts)); + String id = validationContext.resolveSchemaId(subschemaNode); + if (id != null) { + if (id.contains(":")) { + // absolute + currentUri = URI.create(id); + path = SchemaLocation.of(id); + } else { + // relative + String absoluteUri = path.getAbsoluteIri().resolve(id).toString(); + currentUri = URI.create(absoluteUri); + path = SchemaLocation.of(absoluteUri); + } } - path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), fragment); - } else { - // absolute - path = SchemaLocation.of(refValue); } - final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, parent); - ref = new JsonSchemaRef(schema); - validationContext.setReferenceParsingInProgress(refValueOriginal, ref); + String[] parts = refValue.split("/"); + for (int x = 1; x < parts.length; x++) { + path = path.append(parts[x]); + } + } else if(refValue.contains(":")) { + // absolute + path = SchemaLocation.of(refValue); + } else { + // relative to lexical root + String id = parent.findSchemaResourceRoot().getId(); + path = SchemaLocation.of(id); + String[] parts = refValue.split("/"); + for (int x = 1; x < parts.length; x++) { + path = path.append(parts[x]); + } } - return ref; + return validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); } - return null; + throw null; } private static URI determineSchemaUri(final URIFactory uriFactory, final JsonSchema parentSchema, final String refUri) { URI schemaUri; - final URI currentUri = parentSchema.getCurrentUri(); + // $ref prevents a sibling $id from changing the base uri + JsonSchema parent = parentSchema.getParentSchema(); // just the parentSchema is the sibling $id with this $ref + final URI currentUri = parent != null ? parent.getCurrentUri() : parentSchema.getCurrentUri(); try { if (currentUri == null) { schemaUri = uriFactory.create(refUri); @@ -162,20 +230,20 @@ private static URI determineSchemaUrn(final URNFactory urnFactory, final String 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); - // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, - // these schemas will be cached along with config. We have to replace the config for cached $ref references - // with the latest config. Reset the config. - this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); - if (this.schema != null) { - errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); - } else { - errors = Collections.emptySet(); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new JsonSchemaException(validationMessage); } + errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); } finally { Scope scope = collectorContext.exitDynamicScope(); if (errors.isEmpty()) { @@ -189,7 +257,7 @@ 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 { @@ -197,10 +265,15 @@ public Set walk(ExecutionContext executionContext, JsonNode n // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, // these schemas will be cached along with config. We have to replace the config for cached $ref references // with the latest config. Reset the config. - this.schema.getSchema().getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); - if (this.schema != null) { - errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new JsonSchemaException(validationMessage); } + errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); return errors; } finally { Scope scope = collectorContext.exitDynamicScope(); @@ -219,6 +292,14 @@ public JsonSchemaRef getSchemaRef() { @Override public void preloadJsonSchema() { - this.schema.getSchema().initializeValidators(); + JsonSchema jsonSchema = null; + try { + jsonSchema = this.schema.getSchema(); + } catch (JsonSchemaException e) { + throw e; + } catch (RuntimeException e) { + throw new JsonSchemaException(e); + } + jsonSchema.initializeValidators(); } } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index e258206b3..1e465b05f 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -16,13 +16,12 @@ package com.networknt.schema; -import java.util.HashMap; -import java.util.Map; +import java.net.URI; import java.util.Optional; -import java.util.Stack; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.uri.URIFactory; import com.networknt.schema.urn.URNFactory; @@ -32,12 +31,19 @@ public class ValidationContext { private final URNFactory urnFactory; private final JsonMetaSchema metaSchema; private final JsonSchemaFactory jsonSchemaFactory; - private SchemaValidatorsConfig config; - private final Map refParsingInProgress = new HashMap<>(); - private final Stack discriminatorContexts = new Stack<>(); + private final SchemaValidatorsConfig config; + private final ConcurrentMap schemaReferences; + private final ConcurrentMap schemaResources; public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, - JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { + JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { + this(uriFactory, urnFactory, metaSchema, jsonSchemaFactory, config, new ConcurrentHashMap<>(), + new ConcurrentHashMap<>()); + } + + public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, + JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config, + ConcurrentMap schemaReferences, ConcurrentMap schemaResources) { if (uriFactory == null) { throw new IllegalArgumentException("URIFactory must not be null"); } @@ -51,13 +57,19 @@ public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaS this.urnFactory = urnFactory; this.metaSchema = metaSchema; this.jsonSchemaFactory = jsonSchemaFactory; - this.config = config; + this.config = config == null ? new SchemaValidatorsConfig() : config; + this.schemaReferences = schemaReferences; + this.schemaResources = schemaResources; } public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema) { return getJsonSchemaFactory().create(this, schemaLocation, evaluationPath, schemaNode, parentSchema); } + public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI currentUri, JsonNode schemaNode, JsonSchema parentSchema) { + return getJsonSchemaFactory().create(this, schemaLocation, evaluationPath, currentUri, schemaNode, parentSchema); + } + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword /* keyword */, JsonNode schemaNode, JsonSchema parentSchema) { return this.metaSchema.newValidator(this, schemaLocation, evaluationPath, keyword, schemaNode, parentSchema); @@ -80,37 +92,25 @@ public JsonSchemaFactory getJsonSchemaFactory() { } public SchemaValidatorsConfig getConfig() { - if (this.config == null) { - this.config = new SchemaValidatorsConfig(); - } return this.config; } - public void setConfig(SchemaValidatorsConfig config) { - this.config = config; - } - - public void setReferenceParsingInProgress(String refValue, JsonSchemaRef ref) { - this.refParsingInProgress.put(refValue, ref); - } - - public JsonSchemaRef getReferenceParsingInProgress(String refValue) { - return this.refParsingInProgress.get(refValue); - } - - public DiscriminatorContext getCurrentDiscriminatorContext() { - if (!this.discriminatorContexts.empty()) { - return this.discriminatorContexts.peek(); - } - return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf - } - - public void enterDiscriminatorContext(final DiscriminatorContext ctx, @SuppressWarnings("unused") JsonNodePath instanceLocation) { - this.discriminatorContexts.push(ctx); + /** + * Gets the schema references identified by the ref uri. + * + * @return the schema references + */ + public ConcurrentMap getSchemaReferences() { + return this.schemaReferences; } - public void leaveDiscriminatorContextImmediately(@SuppressWarnings("unused") JsonNodePath instanceLocation) { - this.discriminatorContexts.pop(); + /** + * Gets the schema resources identified by id. + * + * @return the schema resources + */ + public ConcurrentMap getSchemaResources() { + return this.schemaResources; } public JsonMetaSchema getMetaSchema() { @@ -121,39 +121,4 @@ public Optional activeDialect() { String metaSchema = getMetaSchema().getUri(); return SpecVersionDetector.detectOptionalVersion(metaSchema); } - - public static class DiscriminatorContext { - private final Map discriminators = new HashMap<>(); - - private boolean discriminatorMatchFound = false; - - public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) { - this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator); - } - - public ObjectNode getDiscriminatorForPath(final SchemaLocation schemaLocation) { - return this.discriminators.get("#" + schemaLocation.getFragment().toString()); - } - - public ObjectNode getDiscriminatorForPath(final String schemaLocation) { - return this.discriminators.get(schemaLocation); - } - - public void markMatch() { - this.discriminatorMatchFound = true; - } - - public boolean isDiscriminatorMatchFound() { - return this.discriminatorMatchFound; - } - - /** - * Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure - * - * @return true in case there are discriminator candidates - */ - public boolean isActive() { - return !this.discriminators.isEmpty(); - } - } } diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 699b521bd..663c036c2 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -16,6 +16,7 @@ public abstract class ValidationMessageHandler { protected SchemaLocation schemaLocation; protected JsonNodePath evaluationPath; + protected JsonSchema evaluationParentSchema; protected JsonSchema parentSchema; @@ -37,6 +38,24 @@ protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessa updateKeyword(keyword); } + /** + * Copy constructor. + * + * @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; + this.evaluationPath = copy.evaluationPath; + this.parentSchema = copy.parentSchema; + this.evaluationParentSchema = copy.evaluationParentSchema; + this.customErrorMessagesEnabled = copy.customErrorMessagesEnabled; + this.errorMessage = copy.errorMessage; + this.keyword = copy.keyword; + } + protected MessageSourceValidationMessage.Builder message() { return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, message -> { if (this.failFast && isApplicator()) { diff --git a/src/test/java/com/networknt/schema/AbsoluteIriTest.java b/src/test/java/com/networknt/schema/AbsoluteIriTest.java index c504b887f..3207ad01c 100644 --- a/src/test/java/com/networknt/schema/AbsoluteIriTest.java +++ b/src/test/java/com/networknt/schema/AbsoluteIriTest.java @@ -63,6 +63,12 @@ void relativeAtRootWithSchemeSpecificPart() { assertEquals("classpath:resource/test.json", iri.resolve("test.json").toString()); } + @Test + void relativeParentWithSchemeSpecificPart() { + AbsoluteIri iri = new AbsoluteIri("classpath:resource/hello/world/testing.json"); + assertEquals("classpath:resource/test.json", iri.resolve("../../test.json").toString()); + } + @Test void rootAbsoluteAtDocument() { AbsoluteIri iri = new AbsoluteIri("http://www.example.org/foo/bar.json"); diff --git a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java index b5dda2764..4ecbd4e37 100644 --- a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java +++ b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicNode; -import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.Test; import java.util.stream.Stream; @@ -22,12 +21,9 @@ void testEmptyPrefixItemsException() { Stream dynamicNodeStream = createTests(SpecVersion.VersionFlag.V7, "src/test/suite/tests/prefixItemsException"); dynamicNodeStream.forEach( dynamicNode -> { - ((DynamicContainer) dynamicNode).getChildren().forEach(dynamicNode1 -> { - if (dynamicNode1 instanceof DynamicTest) { - assertThrows(JsonSchemaException.class, () -> { - ((DynamicTest) dynamicNode1).getExecutable().execute(); - }); - } + assertThrows(JsonSchemaException.class, () -> { + ((DynamicContainer) dynamicNode).getChildren().forEach(dynamicNode1 -> { + }); }); } ); diff --git a/src/test/java/com/networknt/schema/SharedConfigTest.java b/src/test/java/com/networknt/schema/SharedConfigTest.java new file mode 100644 index 000000000..63e63ceb8 --- /dev/null +++ b/src/test/java/com/networknt/schema/SharedConfigTest.java @@ -0,0 +1,54 @@ +package com.networknt.schema; + +import java.net.URI; +import java.util.Set; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.walk.JsonSchemaWalkListener; +import com.networknt.schema.walk.WalkEvent; +import com.networknt.schema.walk.WalkFlow; + +/** + * Issue 918. + */ +public class SharedConfigTest { + private static class AllKeywordListener implements JsonSchemaWalkListener { + public boolean wasCalled = false; + + @Override + public WalkFlow onWalkStart(WalkEvent walkEvent) { + wasCalled = true; + return WalkFlow.CONTINUE; + } + + @Override + public void onWalkEnd(WalkEvent walkEvent, Set validationMessages) { + } + } + + @Test + public void shouldCallAllKeywordListenerOnWalkStart() throws Exception { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); + + SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); + AllKeywordListener allKeywordListener = new AllKeywordListener(); + schemaValidatorsConfig.addKeywordWalkListener(allKeywordListener); + + URI draft07Schema = new URI("resource:/draft-07/schema#"); + + // depending on this line the test either passes or fails: + // - if this line is executed, then it passes + // - if this line is not executed (just comment it) - it fails + JsonSchema firstSchema = factory.getSchema(draft07Schema); + firstSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true); + + // note that only second schema takes overridden schemaValidatorsConfig + JsonSchema secondSchema = factory.getSchema(draft07Schema, schemaValidatorsConfig); + + secondSchema.walk(new ObjectMapper().readTree("{ \"id\": 123 }"), true); + Assertions.assertTrue(allKeywordListener.wasCalled); + } +} \ No newline at end of file diff --git a/src/test/suite/tests/draft-next/enum.json b/src/test/suite/tests/draft-next/enum.json index 32e5af01b..e263f3901 100644 --- a/src/test/suite/tests/draft-next/enum.json +++ b/src/test/suite/tests/draft-next/enum.json @@ -168,6 +168,30 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "enum": [[false]] + }, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": { @@ -192,6 +216,30 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "enum": [[true]] + }, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": { @@ -216,6 +264,30 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "enum": [[0]] + }, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": { @@ -240,6 +312,30 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "enum": [[1]] + }, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { diff --git a/src/test/suite/tests/draft2019-09/enum.json b/src/test/suite/tests/draft2019-09/enum.json index f9a44a61d..1315211ea 100644 --- a/src/test/suite/tests/draft2019-09/enum.json +++ b/src/test/suite/tests/draft2019-09/enum.json @@ -168,6 +168,30 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "enum": [[false]] + }, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": { @@ -192,6 +216,30 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "enum": [[true]] + }, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": { @@ -216,6 +264,30 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "enum": [[0]] + }, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": { @@ -240,6 +312,30 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "enum": [[1]] + }, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { diff --git a/src/test/suite/tests/draft2019-09/ref.json b/src/test/suite/tests/draft2019-09/ref.json index ac9971c1c..95a73345d 100644 --- a/src/test/suite/tests/draft2019-09/ref.json +++ b/src/test/suite/tests/draft2019-09/ref.json @@ -309,7 +309,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -475,7 +475,7 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -530,7 +530,7 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -589,7 +589,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -624,7 +624,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -684,7 +684,7 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -830,7 +830,7 @@ "bar": {"type": "string"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -860,7 +860,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -887,7 +887,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -911,7 +911,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -935,7 +935,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -959,7 +959,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -990,7 +990,7 @@ }, "$ref": "/absref/foobar.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { diff --git a/src/test/suite/tests/draft2019-09/refRemote.json b/src/test/suite/tests/draft2019-09/refRemote.json index 63ace6c9b..b84dad69a 100644 --- a/src/test/suite/tests/draft2019-09/refRemote.json +++ b/src/test/suite/tests/draft2019-09/refRemote.json @@ -113,7 +113,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { diff --git a/src/test/suite/tests/draft2020-12/enum.json b/src/test/suite/tests/draft2020-12/enum.json index 0d780b2ac..c8f35eacf 100644 --- a/src/test/suite/tests/draft2020-12/enum.json +++ b/src/test/suite/tests/draft2020-12/enum.json @@ -168,6 +168,30 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [[false]] + }, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": { @@ -192,6 +216,30 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [[true]] + }, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": { @@ -216,6 +264,30 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [[0]] + }, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": { @@ -240,6 +312,30 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [[1]] + }, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { diff --git a/src/test/suite/tests/draft2020-12/ref.json b/src/test/suite/tests/draft2020-12/ref.json index 7eefef6f2..7ceb50e6e 100644 --- a/src/test/suite/tests/draft2020-12/ref.json +++ b/src/test/suite/tests/draft2020-12/ref.json @@ -309,7 +309,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -475,7 +475,7 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -530,7 +530,7 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -589,7 +589,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -624,7 +624,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -684,7 +684,7 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -830,7 +830,7 @@ "bar": {"type": "string"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -860,7 +860,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -887,7 +887,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -911,7 +911,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -935,7 +935,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -959,7 +959,7 @@ "type": "integer" } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -990,7 +990,7 @@ }, "$ref": "/absref/foobar.json" }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { diff --git a/src/test/suite/tests/draft2020-12/refRemote.json b/src/test/suite/tests/draft2020-12/refRemote.json index 7689f551d..5508e357f 100644 --- a/src/test/suite/tests/draft2020-12/refRemote.json +++ b/src/test/suite/tests/draft2020-12/refRemote.json @@ -113,7 +113,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { diff --git a/src/test/suite/tests/draft4/enum.json b/src/test/suite/tests/draft4/enum.json index f085097be..ce43acc02 100644 --- a/src/test/suite/tests/draft4/enum.json +++ b/src/test/suite/tests/draft4/enum.json @@ -154,6 +154,27 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": {"enum": [[false]]}, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": {"enum": [true]}, @@ -175,6 +196,27 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": {"enum": [[true]]}, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": {"enum": [0]}, @@ -196,6 +238,27 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": {"enum": [[0]]}, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": {"enum": [1]}, @@ -217,6 +280,27 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": {"enum": [[1]]}, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { "enum": [ "hello\u0000there" ] }, diff --git a/src/test/suite/tests/draft4/ref.json b/src/test/suite/tests/draft4/ref.json index 81155671e..4b170eb34 100644 --- a/src/test/suite/tests/draft4/ref.json +++ b/src/test/suite/tests/draft4/ref.json @@ -198,7 +198,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -301,7 +301,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -436,7 +436,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -497,7 +497,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { diff --git a/src/test/suite/tests/draft4/refRemote.json b/src/test/suite/tests/draft4/refRemote.json index 88e1b18bf..fb1d03cfe 100644 --- a/src/test/suite/tests/draft4/refRemote.json +++ b/src/test/suite/tests/draft4/refRemote.json @@ -120,7 +120,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { diff --git a/src/test/suite/tests/draft6/enum.json b/src/test/suite/tests/draft6/enum.json index f085097be..ce43acc02 100644 --- a/src/test/suite/tests/draft6/enum.json +++ b/src/test/suite/tests/draft6/enum.json @@ -154,6 +154,27 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": {"enum": [[false]]}, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": {"enum": [true]}, @@ -175,6 +196,27 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": {"enum": [[true]]}, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": {"enum": [0]}, @@ -196,6 +238,27 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": {"enum": [[0]]}, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": {"enum": [1]}, @@ -217,6 +280,27 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": {"enum": [[1]]}, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { "enum": [ "hello\u0000there" ] }, diff --git a/src/test/suite/tests/draft6/ref.json b/src/test/suite/tests/draft6/ref.json index 7ade44154..ed9fe56a5 100644 --- a/src/test/suite/tests/draft6/ref.json +++ b/src/test/suite/tests/draft6/ref.json @@ -198,7 +198,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -333,7 +333,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -468,7 +468,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -530,7 +530,7 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -584,7 +584,7 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -629,7 +629,7 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -755,7 +755,7 @@ "bar": {"type": "string"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -784,7 +784,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -817,7 +817,7 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { diff --git a/src/test/suite/tests/draft6/refRemote.json b/src/test/suite/tests/draft6/refRemote.json index 2add42c92..22baff6d3 100644 --- a/src/test/suite/tests/draft6/refRemote.json +++ b/src/test/suite/tests/draft6/refRemote.json @@ -120,7 +120,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { diff --git a/src/test/suite/tests/draft7/enum.json b/src/test/suite/tests/draft7/enum.json index f085097be..ce43acc02 100644 --- a/src/test/suite/tests/draft7/enum.json +++ b/src/test/suite/tests/draft7/enum.json @@ -154,6 +154,27 @@ } ] }, + { + "description": "enum with [false] does not match [0]", + "schema": {"enum": [[false]]}, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, { "description": "enum with true does not match 1", "schema": {"enum": [true]}, @@ -175,6 +196,27 @@ } ] }, + { + "description": "enum with [true] does not match [1]", + "schema": {"enum": [[true]]}, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, { "description": "enum with 0 does not match false", "schema": {"enum": [0]}, @@ -196,6 +238,27 @@ } ] }, + { + "description": "enum with [0] does not match [false]", + "schema": {"enum": [[0]]}, + "tests": [ + { + "description": "[false] is invalid", + "data": [false], + "valid": false + }, + { + "description": "[0] is valid", + "data": [0], + "valid": true + }, + { + "description": "[0.0] is valid", + "data": [0.0], + "valid": true + } + ] + }, { "description": "enum with 1 does not match true", "schema": {"enum": [1]}, @@ -217,6 +280,27 @@ } ] }, + { + "description": "enum with [1] does not match [true]", + "schema": {"enum": [[1]]}, + "tests": [ + { + "description": "[true] is invalid", + "data": [true], + "valid": false + }, + { + "description": "[1] is valid", + "data": [1], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [1.0], + "valid": true + } + ] + }, { "description": "nul characters in strings", "schema": { "enum": [ "hello\u0000there" ] }, diff --git a/src/test/suite/tests/draft7/ref.json b/src/test/suite/tests/draft7/ref.json index fe5d9d40f..82c1e8c24 100644 --- a/src/test/suite/tests/draft7/ref.json +++ b/src/test/suite/tests/draft7/ref.json @@ -198,7 +198,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -333,7 +333,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -468,7 +468,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -530,7 +530,7 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -584,7 +584,7 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -642,7 +642,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -667,7 +667,7 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -793,7 +793,7 @@ "bar": {"type": "string"} } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -822,7 +822,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -850,7 +850,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -878,7 +878,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -906,7 +906,7 @@ } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { @@ -939,7 +939,7 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": true, + "disabled": false, "reason": "Schema resources are currently unsupported. See #503", "tests": [ { diff --git a/src/test/suite/tests/draft7/refRemote.json b/src/test/suite/tests/draft7/refRemote.json index 2add42c92..22baff6d3 100644 --- a/src/test/suite/tests/draft7/refRemote.json +++ b/src/test/suite/tests/draft7/refRemote.json @@ -120,7 +120,7 @@ } } }, - "disabled": true, + "disabled": false, "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ {