From 42658a8858904a850ae1227c08c9b3c4a6ef9fa3 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 26 Dec 2023 16:48:22 +0800 Subject: [PATCH 01/24] Support schema resources --- .../com/networknt/schema/AllOfValidator.java | 5 ++ .../com/networknt/schema/AnyOfValidator.java | 5 ++ .../com/networknt/schema/CachedSupplier.java | 41 ++++++++++ .../java/com/networknt/schema/JsonSchema.java | 79 ++++++++++++++++--- .../com/networknt/schema/JsonSchemaRef.java | 13 +-- .../schema/NonValidationKeyword.java | 10 ++- .../com/networknt/schema/NotValidator.java | 4 + .../com/networknt/schema/OneOfValidator.java | 5 ++ .../com/networknt/schema/RefValidator.java | 60 ++++++++++---- .../networknt/schema/ValidationContext.java | 6 ++ src/test/suite/tests/draft2019-09/ref.json | 26 +++--- src/test/suite/tests/draft2020-12/ref.json | 26 +++--- src/test/suite/tests/draft4/ref.json | 8 +- src/test/suite/tests/draft6/ref.json | 18 ++--- src/test/suite/tests/draft7/ref.json | 26 +++--- 15 files changed, 247 insertions(+), 85 deletions(-) create mode 100644 src/main/java/com/networknt/schema/CachedSupplier.java diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 30074a548..a2a80df47 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -38,6 +38,11 @@ public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), schemaNode.get(i), parentSchema)); } + for (JsonSchema schema : this.schemas) { + // Load the validators to parse the schema so that schema resources with $id can + // be identified + schema.getValidators(); + } } @Override diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 99ffb905a..3c5a725a0 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -46,6 +46,11 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath } else { this.discriminatorContext = null; } + for (JsonSchema schema : this.schemas) { + // Load the validators to parse the schema so that schema resources with $id can + // be identified + schema.getValidators(); + } } @Override 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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index b142ac9aa..fd4a54ae3 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -57,12 +57,13 @@ 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; + 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,9 +75,19 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc this.validationContext = validationContext; this.metaSchema = validationContext.getMetaSchema(); this.currentUri = combineCurrentUriWithIds(currentUri, schemaNode); + if (uriRefersToSubschema(currentUri, schemaLocation)) { updateThisAsSubschema(currentUri); } + if (this.currentUri != null) { + this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString(), this); + } + String idKeyword = this.validationContext.getMetaSchema().getIdKeyword(); + if (idKeyword != null) { + readDefinitions(idKeyword, "definitions"); + readDefinitions(idKeyword, "$defs"); + } + if (validationContext.getConfig() != null) { this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap()); if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { @@ -86,6 +97,7 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc } } } + this.id = validationContext.resolveSchemaId(this.schemaNode); } public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { @@ -120,7 +132,7 @@ private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { } private static boolean isUriFragmentWithNoContext(URI currentUri, String id) { - return id.startsWith("#") && currentUri == null; + return id.startsWith("#") && (currentUri == null || currentUri.toString().startsWith("#")); } private static boolean uriRefersToSubschema(URI originalUri, SchemaLocation schemaLocation) { @@ -163,12 +175,16 @@ public URI getCurrentUri() { * @return JsonNode */ public JsonNode getRefSchemaNode(String ref) { - JsonSchema schema = findAncestor(); + JsonSchema schema = findLexicalRoot(); 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("/")) { @@ -193,15 +209,19 @@ public JsonNode getRefSchemaNode(String ref) { } // 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; } + public String getId() { + return this.id; + } + public JsonSchema findAncestor() { JsonSchema ancestor = this; if (this.getParentSchema() != null) { @@ -217,6 +237,50 @@ private JsonNode handleNullNode(String ref, JsonSchema schema) { } return null; } + + private void readDefinitions(String idKeyword, String definitionsKeyword) { + JsonNode definitionsNode = schemaNode.get(definitionsKeyword); + if (definitionsNode != null) { + readSchemaResources(idKeyword, definitionsKeyword, definitionsNode, + this.schemaLocation.resolve(definitionsKeyword), this.evaluationPath.resolve(definitionsKeyword), + this.currentUri); + } + } + + private void readSchemaResources(String idKeyword, String definitionsKeyword, JsonNode schemaNode, + SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI idUri) { + URI currentIdUri = idUri; + JsonNode idNode = schemaNode.get(idKeyword); + if (idNode != null && idNode.isTextual()) { + // This is a schema resource + // $id inside an unknown keyword is not a read identifier + if (definitionsKeyword.equals(evaluationPath.getElement(evaluationPath.getNameCount() - 2))) { + String id = idNode.asText(); + URI uri = null; + if (id.contains(":")) { + uri = URI.create(id); + } else { + uri = idUri; + if (uri == null) { + uri = URI.create(""); + } + uri = uri.resolve(id); + } + currentIdUri = uri; + JsonSchema resource = new JsonSchema(validationContext, schemaLocation, evaluationPath, uri, schemaNode, + this, true); + this.validationContext.getSchemaResources().put(uri.toString(), resource); + } + } + + Iterator pnames = schemaNode.fieldNames(); + while (pnames.hasNext()) { + String pname = pnames.next(); + JsonNode nodeToUse = schemaNode.get(pname); + readSchemaResources(idKeyword, definitionsKeyword, nodeToUse, schemaLocation.resolve(pname), evaluationPath.resolve(pname), currentIdUri); + } + } + /** * Please note that the key in {@link #validators} map is the evaluation path. @@ -236,9 +300,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(); diff --git a/src/main/java/com/networknt/schema/JsonSchemaRef.java b/src/main/java/com/networknt/schema/JsonSchemaRef.java index 77b3d2114..a966c0ec9 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaRef.java +++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Set; +import java.util.function.Supplier; /** * Use this object instead a JsonSchema for references. @@ -28,23 +29,23 @@ public class JsonSchemaRef { - private final JsonSchema schema; + private final Supplier schemaSupplier; - public JsonSchemaRef(JsonSchema schema) { - this.schema = schema; + public JsonSchemaRef(Supplier schema) { + this.schemaSupplier = schema; } public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - return schema.validate(executionContext, node, rootNode, instanceLocation); + return getSchema().validate(executionContext, node, rootNode, instanceLocation); } public JsonSchema getSchema() { - return schema; + return this.schemaSupplier.get(); } public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - return schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + return getSchema().walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index 45f8cfc8d..aee27a1c6 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -27,8 +27,14 @@ 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); + if (id != null) { + // Used to register schema resources with $id + validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); + } } @Override @@ -44,6 +50,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/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 6e5164301..2e00b530d 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -32,6 +32,10 @@ public class NotValidator extends BaseJsonValidator { public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext); this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); + + // Load the validators to parse the schema so that schema resources with $id can + // be identified + this.schema.getValidators(); } @Override diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index e7e8eb7b4..ffaf95b12 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -36,6 +36,11 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath JsonNode childNode = schemaNode.get(i); this.schemas.add(validationContext.newSchema( schemaLocation.append(i), evaluationPath.append(i), childNode, parentSchema)); } + for (JsonSchema schema : this.schemas) { + // Load the validators to parse the schema so that schema resources with $id can + // be identified + schema.getValidators(); + } } @Override diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 5f0649a09..7ab75dc65 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -26,6 +26,7 @@ import java.net.URI; import java.util.*; +import java.util.function.Supplier; public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); @@ -45,7 +46,8 @@ public RefValidator(SchemaLocation schemaLocation, JsonNodePath 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(); + .instanceLocation(schemaLocation.getFragment()).evaluationPath(schemaLocation.getFragment()) + .arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } } @@ -55,7 +57,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; - JsonSchema parent = parentSchema; + Supplier 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 +70,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(), parent.get(), refUri); if (schemaUri == null) { // the URNFactory is optional if (validationContext.getURNFactory() == null) { @@ -81,51 +83,67 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } } else if (URN_SCHEME.equals(schemaUri.getScheme())) { // Try to resolve URN schema as a JsonSchemaRef to some sub-schema of the parent - JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, schemaUri.toString(), refValueOriginal, evaluationPath); + JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, refValue, 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()); - + parent = new CachedSupplier<>(() -> { + JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); + if (schemaResource != null) { + return schemaResource; + } + return validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()) + .findAncestor(); + }); if (index < 0) { - return new JsonSchemaRef(parent.findAncestor()); + return new JsonSchemaRef(parent); } refValue = refValue.substring(index); } if (refValue.equals(REF_CURRENT)) { - return new JsonSchemaRef(parent.findAncestor()); + Supplier supplier = parent; + return new JsonSchemaRef(() -> supplier.get().findAncestor()); } return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); } - private static JsonSchemaRef getJsonSchemaRef(JsonSchema parent, + private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplier, ValidationContext validationContext, String refValue, String refValueOriginal, JsonNodePath evaluationPath) { + JsonSchema parent = parentSupplier.get(); 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); + // relative to document String[] parts = refValue.split("/"); + JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); for (int x = 1; x < parts.length; x++) { fragment = fragment.append(parts[x]); } path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), fragment); - } else { + } else if(refValue.contains(":")) { // absolute path = SchemaLocation.of(refValue); + } else { + // relative to lexical root + String id = parent.findLexicalRoot().getId(); + path = SchemaLocation.of(id); + String[] parts = refValue.split("/"); + for (int x = 1; x < parts.length; x++) { + path = path.resolve(parts[x]); + } } final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, parent); - ref = new JsonSchemaRef(schema); + ref = new JsonSchemaRef(() -> schema); validationContext.setReferenceParsingInProgress(refValueOriginal, ref); } return ref; @@ -135,7 +153,9 @@ private static JsonSchemaRef getJsonSchemaRef(JsonSchema parent, 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); @@ -219,6 +239,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..da955f915 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Optional; import java.util.Stack; +import java.util.concurrent.ConcurrentHashMap; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -35,6 +36,7 @@ public class ValidationContext { private SchemaValidatorsConfig config; private final Map refParsingInProgress = new HashMap<>(); private final Stack discriminatorContexts = new Stack<>(); + private final Map schemaResources = new ConcurrentHashMap<>(); public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { @@ -98,6 +100,10 @@ public JsonSchemaRef getReferenceParsingInProgress(String refValue) { return this.refParsingInProgress.get(refValue); } + public Map getSchemaResources() { + return this.schemaResources; + } + public DiscriminatorContext getCurrentDiscriminatorContext() { if (!this.discriminatorContexts.empty()) { return this.discriminatorContexts.peek(); 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/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/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/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/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": [ { From 67ed4450e5ab05fe65f9cce113b8758f1dc1e152 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:46:04 +0800 Subject: [PATCH 02/24] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 7 +++--- .../networknt/schema/JsonSchemaFactory.java | 2 +- .../com/networknt/schema/JsonSchemaRef.java | 4 --- .../com/networknt/schema/RefValidator.java | 9 +++---- .../networknt/schema/ValidationContext.java | 25 ++++++++++++------- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index fd4a54ae3..6398bcd52 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -79,9 +79,6 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc if (uriRefersToSubschema(currentUri, schemaLocation)) { updateThisAsSubschema(currentUri); } - if (this.currentUri != null) { - this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString(), this); - } String idKeyword = this.validationContext.getMetaSchema().getIdKeyword(); if (idKeyword != null) { readDefinitions(idKeyword, "definitions"); @@ -98,6 +95,10 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc } } this.id = validationContext.resolveSchemaId(this.schemaNode); + if (this.id != null) { + this.validationContext.getSchemaResources() + .putIfAbsent(this.currentUri != null ? this.currentUri.toString() : this.id, this); + } } public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index b43b190c2..896254dad 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; diff --git a/src/main/java/com/networknt/schema/JsonSchemaRef.java b/src/main/java/com/networknt/schema/JsonSchemaRef.java index a966c0ec9..f4ebdd7b6 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaRef.java +++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java @@ -22,11 +22,7 @@ /** * 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 Supplier schemaSupplier; diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 7ab75dc65..7865a1812 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -119,8 +119,7 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie JsonSchema parent = parentSupplier.get(); JsonNode node = parent.getRefSchemaNode(refValue); if (node != null) { - JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal); - if (ref == null) { + return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { SchemaLocation path = null; if (refValue.startsWith(REF_CURRENT)) { // relative to document @@ -143,10 +142,8 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie } } final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, parent); - ref = new JsonSchemaRef(() -> schema); - validationContext.setReferenceParsingInProgress(refValueOriginal, ref); - } - return ref; + return new JsonSchemaRef(() -> schema); + }); } return null; } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index da955f915..778590a94 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -21,6 +21,7 @@ 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; @@ -34,9 +35,9 @@ public class ValidationContext { 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 Map schemaResources = new ConcurrentHashMap<>(); + private final ConcurrentMap schemaReferences = new ConcurrentHashMap<>(); + private final ConcurrentMap schemaResources = new ConcurrentHashMap<>(); public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) { @@ -92,15 +93,21 @@ public void setConfig(SchemaValidatorsConfig config) { this.config = config; } - public void setReferenceParsingInProgress(String refValue, JsonSchemaRef ref) { - this.refParsingInProgress.put(refValue, ref); + /** + * Gets the schema references identified by the ref uri. + * + * @return the schema references + */ + public ConcurrentMap getSchemaReferences() { + return this.schemaReferences; } - public JsonSchemaRef getReferenceParsingInProgress(String refValue) { - return this.refParsingInProgress.get(refValue); - } - - public Map getSchemaResources() { + /** + * Gets the schema resources identified by id. + * + * @return the schema resources + */ + public ConcurrentMap getSchemaResources() { return this.schemaResources; } From 4c14c3527489379cfeeeeb03dd59437263c870ff Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 27 Dec 2023 19:32:52 +0800 Subject: [PATCH 03/24] Refactor --- .../networknt/schema/JsonSchemaFactory.java | 105 ++++++++---------- 1 file changed, 49 insertions(+), 56 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 896254dad..f3f80c823 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -421,68 +421,61 @@ 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); + }); + // 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; + } + 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 */); - } 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(); + 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 */); + } else { + final ValidationContext validationContext = createValidationContext(schemaNode); + validationContext.setConfig(config); + jsonSchema = doCreate(validationContext, SchemaLocation.of(""), evaluationPath, mappedUri, schemaNode, null, false); } - } catch (IOException ioe) { - logger.error("Failed to load json schema!", ioe); - throw new JsonSchemaException(ioe); + return jsonSchema; + } catch (IOException e) { + logger.error("Failed to load json schema!", e); + throw new JsonSchemaException(e); } } From 2f4f592d6a8d7973a26fc98d6962d464b1805b84 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 10:03:21 +0800 Subject: [PATCH 04/24] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 20 +++------- .../com/networknt/schema/RefValidator.java | 37 ++++++++++++++++--- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 6398bcd52..e32d4d85b 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -256,21 +256,11 @@ private void readSchemaResources(String idKeyword, String definitionsKeyword, Js // This is a schema resource // $id inside an unknown keyword is not a read identifier if (definitionsKeyword.equals(evaluationPath.getElement(evaluationPath.getNameCount() - 2))) { - String id = idNode.asText(); - URI uri = null; - if (id.contains(":")) { - uri = URI.create(id); - } else { - uri = idUri; - if (uri == null) { - uri = URI.create(""); - } - uri = uri.resolve(id); - } - currentIdUri = uri; - JsonSchema resource = new JsonSchema(validationContext, schemaLocation, evaluationPath, uri, schemaNode, - this, true); - this.validationContext.getSchemaResources().put(uri.toString(), resource); + // The schema resource will be registered in the JsonSchema constructor + // The combineCurrentUriWithIds will combine the uri with the id to get the new currentIdUri + JsonSchema schemaResource = new JsonSchema(validationContext, schemaLocation, evaluationPath, idUri, schemaNode, this, + true); + currentIdUri = schemaResource.getCurrentUri(); } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 7865a1812..c29057d71 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -118,17 +118,42 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie JsonNodePath evaluationPath) { JsonSchema parent = parentSupplier.get(); JsonNode node = parent.getRefSchemaNode(refValue); + if (node != null) { return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { - SchemaLocation path = null; + SchemaLocation path = parent.schemaLocation; + JsonSchema currentParent = parent; if (refValue.startsWith(REF_CURRENT)) { // relative to document + // Attempt to get subschema node + String[] refParts = refValue.split("/"); + if (refParts.length > 2) { + 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 + JsonSchema subschema = validationContext.getSchemaResources().get(id); + if (subschema != null) { + currentParent = subschema; + } + path = SchemaLocation.of(id + "#"); + } else { + // relative + JsonSchema subschema = validationContext.getSchemaResources() + .get(path.getFragment().getParent().append(id).toString()); + if (subschema != null) { + currentParent = subschema; + } + path = SchemaLocation.of(id + "#"); + } + } + } String[] parts = refValue.split("/"); - JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); for (int x = 1; x < parts.length; x++) { - fragment = fragment.append(parts[x]); + path = path.append(parts[x]); } - path = new SchemaLocation(parent.schemaLocation.getAbsoluteIri(), fragment); } else if(refValue.contains(":")) { // absolute path = SchemaLocation.of(refValue); @@ -138,10 +163,10 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie path = SchemaLocation.of(id); String[] parts = refValue.split("/"); for (int x = 1; x < parts.length; x++) { - path = path.resolve(parts[x]); + path = path.append(parts[x]); } } - final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, parent); + final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, currentParent); return new JsonSchemaRef(() -> schema); }); } From bde9aa6d7b85d09f48452fcdd2a98790ceb2cf5e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:09:28 +0800 Subject: [PATCH 05/24] Support uri change in id --- .../java/com/networknt/schema/JsonSchema.java | 4 ++-- .../networknt/schema/JsonSchemaFactory.java | 14 +++++++----- .../com/networknt/schema/RefValidator.java | 22 ++++++++----------- .../networknt/schema/ValidationContext.java | 5 +++++ .../suite/tests/draft2019-09/refRemote.json | 2 +- .../suite/tests/draft2020-12/refRemote.json | 2 +- src/test/suite/tests/draft4/refRemote.json | 2 +- src/test/suite/tests/draft6/refRemote.json | 2 +- src/test/suite/tests/draft7/refRemote.json | 2 +- 9 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index e32d4d85b..b7bfbc79a 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -243,7 +243,7 @@ private void readDefinitions(String idKeyword, String definitionsKeyword) { JsonNode definitionsNode = schemaNode.get(definitionsKeyword); if (definitionsNode != null) { readSchemaResources(idKeyword, definitionsKeyword, definitionsNode, - this.schemaLocation.resolve(definitionsKeyword), this.evaluationPath.resolve(definitionsKeyword), + this.schemaLocation.append(definitionsKeyword), this.evaluationPath.append(definitionsKeyword), this.currentUri); } } @@ -268,7 +268,7 @@ private void readSchemaResources(String idKeyword, String definitionsKeyword, Js while (pnames.hasNext()) { String pname = pnames.next(); JsonNode nodeToUse = schemaNode.get(pname); - readSchemaResources(idKeyword, definitionsKeyword, nodeToUse, schemaLocation.resolve(pname), evaluationPath.resolve(pname), currentIdUri); + readSchemaResources(idKeyword, definitionsKeyword, nodeToUse, schemaLocation.append(pname), evaluationPath.append(pname), currentIdUri); } } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index f3f80c823..c51e49554 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -330,17 +330,21 @@ protected JsonSchema newJsonSchema(final URI schemaUri, final JsonNode schemaNod 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. * diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index c29057d71..0aac7dcb2 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -121,32 +121,28 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie if (node != null) { return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { - SchemaLocation path = parent.schemaLocation; + 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 > 2) { + 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 - JsonSchema subschema = validationContext.getSchemaResources().get(id); - if (subschema != null) { - currentParent = subschema; - } + currentUri = URI.create(id); path = SchemaLocation.of(id + "#"); } else { // relative - JsonSchema subschema = validationContext.getSchemaResources() - .get(path.getFragment().getParent().append(id).toString()); - if (subschema != null) { - currentParent = subschema; - } - path = SchemaLocation.of(id + "#"); + currentUri = URI.create(path.append(id).toString()); + path = path.append(id + "#"); } } } @@ -166,7 +162,7 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie path = path.append(parts[x]); } } - final JsonSchema schema = validationContext.newSchema(path, evaluationPath, node, currentParent); + final JsonSchema schema = validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); return new JsonSchemaRef(() -> schema); }); } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 778590a94..80633329e 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -16,6 +16,7 @@ package com.networknt.schema; +import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -61,6 +62,10 @@ public JsonSchema newSchema(SchemaLocation schemaLocation, JsonNodePath evaluati 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); 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/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/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/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/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": [ { From d52216e95292b5c82b73758c7a96fb8b891d8ff0 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 12:27:09 +0800 Subject: [PATCH 06/24] Fix schema resource parent and evaluation path --- .../java/com/networknt/schema/JsonSchema.java | 31 +++++++++++++++++-- .../com/networknt/schema/RefValidator.java | 3 +- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index b7bfbc79a..604f611ae 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -101,6 +101,34 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc } } + private JsonSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, + ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, null, null, validationContext, + suppressSubSchemaRetrieval); + this.validationContext = validationContext; + this.metaSchema = validationContext.getMetaSchema(); + this.id = validationContext.resolveSchemaId(this.schemaNode); + } + + /** + * 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 refParent the parent ref + * @param refEvaluationPath the ref evaluation path + * @return the schema + */ + public JsonSchema fromRef(JsonSchema refParent, JsonNodePath refEvaluationPath) { + JsonSchema copy = new JsonSchema(this.schemaLocation, refEvaluationPath, this.schemaNode, refParent, null, null, this.validationContext, this.suppressSubSchemaRetrieval); + copy.currentUri = this.currentUri; + copy.keywordWalkListenerRunner = this.keywordWalkListenerRunner; + return copy; + } + public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { return getValidationContext().newSchema(schemaLocation, evaluationPath, schemaNode, this); } @@ -121,8 +149,7 @@ private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { } catch (IllegalArgumentException e) { SchemaLocation path = schemaLocation.append(this.metaSchema.getIdKeyword()); ValidationMessage validationMessage = ValidationMessage.builder().code(ValidatorTypeCode.ID.getValue()) - .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()) - .evaluationPath(path.getFragment()) + .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()).evaluationPath(path.getFragment()) .arguments(currentUri == null ? "null" : currentUri.toString(), id) .messageFormatter(args -> this.validationContext.getConfig().getMessageSource().getMessage( ValidatorTypeCode.ID.getValue(), this.validationContext.getConfig().getLocale(), args)) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 0aac7dcb2..f4ed66635 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -94,7 +94,8 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val parent = new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); if (schemaResource != null) { - return schemaResource; + // Schema resource needs to update the parent and evaluation path + return schemaResource.fromRef(parentSchema, evaluationPath); } return validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()) .findAncestor(); From 900e727108e32318ab4f3b999496e09602c191ec Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:07:35 +0800 Subject: [PATCH 07/24] Fix --- .../networknt/schema/BaseJsonValidator.java | 14 +++++++ .../java/com/networknt/schema/JsonSchema.java | 38 +++++++++++++------ .../networknt/schema/JsonSchemaFactory.java | 9 +++++ .../schema/ValidationMessageHandler.java | 17 +++++++++ 4 files changed, 66 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 6de3bf544..24b751dee 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -69,6 +69,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"); diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 604f611ae..df2452aa2 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -101,14 +101,22 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc } } - private JsonSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, - JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, - ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, null, null, validationContext, - suppressSubSchemaRetrieval); - this.validationContext = validationContext; - this.metaSchema = validationContext.getMetaSchema(); - this.id = validationContext.resolveSchemaId(this.schemaNode); + /** + * 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; } /** @@ -123,9 +131,14 @@ private JsonSchema(SchemaLocation schemaLocation, JsonNodePath evaluationPath, J * @return the schema */ public JsonSchema fromRef(JsonSchema refParent, JsonNodePath refEvaluationPath) { - JsonSchema copy = new JsonSchema(this.schemaLocation, refEvaluationPath, this.schemaNode, refParent, null, null, this.validationContext, this.suppressSubSchemaRetrieval); - copy.currentUri = this.currentUri; - copy.keywordWalkListenerRunner = this.keywordWalkListenerRunner; + JsonSchema copy = new JsonSchema(this); + copy.evaluationPath = refEvaluationPath; + copy.parentSchema = refParent; + // Validator state is reset due to the changes in evaluation path + copy.validatorsLoaded = false; + copy.requiredValidator = null; + copy.typeValidator = null; + copy.validators = null; return copy; } @@ -149,7 +162,8 @@ private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) { } catch (IllegalArgumentException e) { SchemaLocation path = schemaLocation.append(this.metaSchema.getIdKeyword()); ValidationMessage validationMessage = ValidationMessage.builder().code(ValidatorTypeCode.ID.getValue()) - .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()).evaluationPath(path.getFragment()) + .type(ValidatorTypeCode.ID.getValue()).instanceLocation(path.getFragment()) + .evaluationPath(path.getFragment()) .arguments(currentUri == null ? "null" : currentUri.toString(), id) .messageFormatter(args -> this.validationContext.getConfig().getMessageSource().getMessage( ValidatorTypeCode.ID.getValue(), this.validationContext.getConfig().getLocale(), args)) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index c51e49554..10bd706ea 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -472,6 +472,15 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig 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 { + String schemaLocationValue = schemaUri.toString(); + String id = jsonMetaSchema.readId(schemaNode); + if (id != null) { + schemaLocationValue = id; + } + if(!schemaLocationValue.contains("#")) { + schemaLocationValue = schemaLocationValue + "#"; + } + SchemaLocation schemaLocation = SchemaLocation.of(schemaLocationValue); final ValidationContext validationContext = createValidationContext(schemaNode); validationContext.setConfig(config); jsonSchema = doCreate(validationContext, SchemaLocation.of(""), evaluationPath, mappedUri, schemaNode, null, false); diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 699b521bd..2510ca9fc 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -37,6 +37,23 @@ 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.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()) { From ce2a6ab429c1aecbf11e3b02be3f4d450cdafd18 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 19:27:06 +0800 Subject: [PATCH 08/24] Fix remote ref paths --- .../java/com/networknt/schema/JsonSchemaFactory.java | 9 +++++++-- src/main/java/com/networknt/schema/RefValidator.java | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 10bd706ea..0880ed27d 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -477,13 +477,18 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig if (id != null) { schemaLocationValue = id; } - if(!schemaLocationValue.contains("#")) { + if (schemaLocationValue.contains("#")) { + // Schema location needs to strip off the fragment as the json schema for the constructor to detect + // it is a subschema + schemaLocationValue = schemaLocationValue.substring(0, schemaLocationValue.indexOf("#") + 1); + } + if (!schemaLocationValue.contains("#")) { schemaLocationValue = schemaLocationValue + "#"; } SchemaLocation schemaLocation = SchemaLocation.of(schemaLocationValue); final ValidationContext validationContext = createValidationContext(schemaNode); validationContext.setConfig(config); - jsonSchema = doCreate(validationContext, SchemaLocation.of(""), evaluationPath, mappedUri, schemaNode, null, false); + jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, false); } return jsonSchema; } catch (IOException e) { diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index f4ed66635..92b4b045f 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -98,7 +98,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return schemaResource.fromRef(parentSchema, evaluationPath); } return validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()) - .findAncestor(); + .findAncestor().fromRef(null, evaluationPath); // Setting the parent affects the lexical root }); if (index < 0) { return new JsonSchemaRef(parent); From b877b3e14aa2b43fa1d413f6666963b8f08c74d5 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 28 Dec 2023 20:39:55 +0800 Subject: [PATCH 09/24] Fix --- src/main/java/com/networknt/schema/JsonSchema.java | 14 ++++++++++++++ .../java/com/networknt/schema/RefValidator.java | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index df2452aa2..1cddf17a8 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -255,6 +255,20 @@ public JsonSchema findLexicalRoot() { JsonSchema ancestor = this; while (ancestor.getId() == null) { if (null == ancestor.getParentSchema()) break; + // The lexical root should not cross + if (ancestor.currentUri != null && ancestor.parentSchema.currentUri == null) break; + if (ancestor.currentUri == null && ancestor.parentSchema.currentUri != null) break; + if (ancestor.getCurrentUri() != null && ancestor.getParentSchema().getCurrentUri() != null) { + if (!Objects.equals(ancestor.getCurrentUri().getScheme(), ancestor.getParentSchema().getCurrentUri().getScheme())) { + break; + } + if (!Objects.equals(ancestor.getCurrentUri().getHost(), ancestor.getParentSchema().getCurrentUri().getHost())) { + break; + } + if (!Objects.equals(ancestor.getCurrentUri().getPath(), ancestor.getParentSchema().getCurrentUri().getPath())) { + break; + } + } ancestor = ancestor.getParentSchema(); } return ancestor; diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 92b4b045f..bc07d9576 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -98,7 +98,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return schemaResource.fromRef(parentSchema, evaluationPath); } return validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()) - .findAncestor().fromRef(null, evaluationPath); // Setting the parent affects the lexical root + .findAncestor().fromRef(parentSchema, evaluationPath); // Setting the parent affects the lexical root }); if (index < 0) { return new JsonSchemaRef(parent); @@ -107,7 +107,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } if (refValue.equals(REF_CURRENT)) { Supplier supplier = parent; - return new JsonSchemaRef(() -> supplier.get().findAncestor()); + return new JsonSchemaRef(() -> supplier.get().findLexicalRoot()); } return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); } From abc4c933418c7481569dce30e79db3788077d0c7 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 29 Dec 2023 01:20:08 +0800 Subject: [PATCH 10/24] Fix schema location --- src/main/java/com/networknt/schema/RefValidator.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index bc07d9576..8cd1d9dad 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -139,11 +139,12 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie if (id.contains(":")) { // absolute currentUri = URI.create(id); - path = SchemaLocation.of(id + "#"); + path = SchemaLocation.of(id); } else { // relative - currentUri = URI.create(path.append(id).toString()); - path = path.append(id + "#"); + String absoluteUri = path.getAbsoluteIri().resolve(id).toString(); + currentUri = URI.create(absoluteUri); + path = SchemaLocation.of(absoluteUri); } } } From 1830c5c99c3f673145e90dba7ea0e5e70f49a2ad Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 29 Dec 2023 10:36:05 +0800 Subject: [PATCH 11/24] Support anchors --- .../com/networknt/schema/JsonMetaSchema.java | 8 +++ .../java/com/networknt/schema/JsonSchema.java | 50 ++++++++----------- .../schema/NonValidationKeyword.java | 3 +- .../com/networknt/schema/RefValidator.java | 20 ++++++++ 4 files changed, 50 insertions(+), 31 deletions(-) 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 1cddf17a8..d120cb991 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -63,6 +63,7 @@ public class JsonSchema extends BaseJsonValidator { 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); @@ -79,12 +80,6 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc if (uriRefersToSubschema(currentUri, schemaLocation)) { updateThisAsSubschema(currentUri); } - String idKeyword = this.validationContext.getMetaSchema().getIdKeyword(); - if (idKeyword != null) { - readDefinitions(idKeyword, "definitions"); - readDefinitions(idKeyword, "$defs"); - } - if (validationContext.getConfig() != null) { this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(this.validationContext.getConfig().getKeywordWalkListenersMap()); if (validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { @@ -95,10 +90,16 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc } } this.id = validationContext.resolveSchemaId(this.schemaNode); + this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); + readDefinitions("definitions"); + readDefinitions("$defs"); 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); + } } /** @@ -117,6 +118,7 @@ protected JsonSchema(JsonSchema copy) { this.typeValidator = copy.typeValidator; this.keywordWalkListenerRunner = copy.keywordWalkListenerRunner; this.id = copy.id; + this.anchor = copy.anchor; } /** @@ -200,7 +202,7 @@ private void updateThisAsSubschema(URI originalUri) { } catch (URISyntaxException ex) { throw new JsonSchemaException("Unable to create URI without fragment from " + this.currentUri + ": " + ex.getMessage()); } - this.parentSchema = new JsonSchema(this.validationContext, SchemaLocation.of(currentUriWithoutFragment.toString()), this.evaluationPath, currentUriWithoutFragment, this.schemaNode, this.parentSchema, super.suppressSubSchemaRetrieval); // TODO: Should this be delegated to the factory? + 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); @@ -294,36 +296,24 @@ private JsonNode handleNullNode(String ref, JsonSchema schema) { return null; } - private void readDefinitions(String idKeyword, String definitionsKeyword) { + private void readDefinitions(String definitionsKeyword) { JsonNode definitionsNode = schemaNode.get(definitionsKeyword); if (definitionsNode != null) { - readSchemaResources(idKeyword, definitionsKeyword, definitionsNode, - this.schemaLocation.append(definitionsKeyword), this.evaluationPath.append(definitionsKeyword), - this.currentUri); + readSchemaResources(definitionsNode, this.schemaLocation.append(definitionsKeyword), + this.evaluationPath.append(definitionsKeyword)); } } - private void readSchemaResources(String idKeyword, String definitionsKeyword, JsonNode schemaNode, - SchemaLocation schemaLocation, JsonNodePath evaluationPath, URI idUri) { - URI currentIdUri = idUri; - JsonNode idNode = schemaNode.get(idKeyword); - if (idNode != null && idNode.isTextual()) { - // This is a schema resource - // $id inside an unknown keyword is not a read identifier - if (definitionsKeyword.equals(evaluationPath.getElement(evaluationPath.getNameCount() - 2))) { - // The schema resource will be registered in the JsonSchema constructor - // The combineCurrentUriWithIds will combine the uri with the id to get the new currentIdUri - JsonSchema schemaResource = new JsonSchema(validationContext, schemaLocation, evaluationPath, idUri, schemaNode, this, - true); - currentIdUri = schemaResource.getCurrentUri(); - } - } - - Iterator pnames = schemaNode.fieldNames(); + private void readSchemaResources(JsonNode definitionsNode, SchemaLocation schemaLocation, JsonNodePath evaluationPath) { + Iterator pnames = definitionsNode.fieldNames(); while (pnames.hasNext()) { String pname = pnames.next(); - JsonNode nodeToUse = schemaNode.get(pname); - readSchemaResources(idKeyword, definitionsKeyword, nodeToUse, schemaLocation.append(pname), evaluationPath.append(pname), currentIdUri); + JsonNode nodeToUse = definitionsNode.get(pname); + // The schema resources with id or anchor will be stored during the constructor + // call of JsonSchema + JsonSchema schema = this.validationContext.newSchema(schemaLocation.append(pname), evaluationPath.append(pname), nodeToUse, + this); + schema.getValidators(); } } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index aee27a1c6..b669572c5 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -31,7 +31,8 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { super(schemaLocation, evaluationPath, keyword); String id = validationContext.resolveSchemaId(schemaNode); - if (id != null) { + String anchor = validationContext.getMetaSchema().readAnchor(schemaNode); + if (id != null || anchor != null) { // Used to register schema resources with $id validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 8cd1d9dad..439dc6934 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -54,6 +54,26 @@ public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, JsonNodePath evaluationPath) { + // $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) { + SchemaLocation schemaLocation = SchemaLocation.of(base.getCurrentUri().toString()); + JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); + if (refValue.startsWith("#")) { + schemaLocation = SchemaLocation.of(schemaLocation.getAbsoluteIri() + refValue); + } else { + schemaLocation = new SchemaLocation(schemaLocation.getAbsoluteIri().resolve(refValue), + fragment); + } + JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaLocation.toString()); + if (schemaResource != null) { + // Schema resource needs to update the parent and evaluation path + return new JsonSchemaRef(() -> schemaResource.fromRef(parentSchema, evaluationPath)); + } + } // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; From f0e2d351c623bd50d8c810415ce59c01d4fecce4 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:46:48 +0800 Subject: [PATCH 12/24] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 68 ++++++++++++++----- .../com/networknt/schema/RefValidator.java | 4 +- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index d120cb991..ad870e2e6 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -134,6 +134,7 @@ protected JsonSchema(JsonSchema copy) { */ public JsonSchema fromRef(JsonSchema refParent, JsonNodePath refEvaluationPath) { JsonSchema copy = new JsonSchema(this); + copy.validationContext.setConfig(refParent.validationContext.getConfig()); copy.evaluationPath = refEvaluationPath; copy.parentSchema = refParent; // Validator state is reset due to the changes in evaluation path @@ -219,7 +220,7 @@ public URI getCurrentUri() { * @return JsonNode */ public JsonNode getRefSchemaNode(String ref) { - JsonSchema schema = findLexicalRoot(); + JsonSchema schema = findSchemaResourceRoot(); JsonNode node = schema.getSchemaNode(); String jsonPointer = ref; @@ -252,30 +253,65 @@ public JsonNode getRefSchemaNode(String ref) { return node; } - // This represents the lexical scope public JsonSchema findLexicalRoot() { JsonSchema ancestor = this; while (ancestor.getId() == null) { if (null == ancestor.getParentSchema()) break; - // The lexical root should not cross - if (ancestor.currentUri != null && ancestor.parentSchema.currentUri == null) break; - if (ancestor.currentUri == null && ancestor.parentSchema.currentUri != null) break; - if (ancestor.getCurrentUri() != null && ancestor.getParentSchema().getCurrentUri() != null) { - if (!Objects.equals(ancestor.getCurrentUri().getScheme(), ancestor.getParentSchema().getCurrentUri().getScheme())) { - break; - } - if (!Objects.equals(ancestor.getCurrentUri().getHost(), ancestor.getParentSchema().getCurrentUri().getHost())) { - break; - } - if (!Objects.equals(ancestor.getCurrentUri().getPath(), ancestor.getParentSchema().getCurrentUri().getPath())) { - 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; } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 439dc6934..8269dba7d 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -127,7 +127,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } if (refValue.equals(REF_CURRENT)) { Supplier supplier = parent; - return new JsonSchemaRef(() -> supplier.get().findLexicalRoot()); + return new JsonSchemaRef(() -> supplier.get().findSchemaResourceRoot()); } return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); } @@ -177,7 +177,7 @@ private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplie path = SchemaLocation.of(refValue); } else { // relative to lexical root - String id = parent.findLexicalRoot().getId(); + String id = parent.findSchemaResourceRoot().getId(); path = SchemaLocation.of(id); String[] parts = refValue.split("/"); for (int x = 1; x < parts.length; x++) { From d17c763ad34eb2e4b539c705756add1c39832072 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:18:10 +0800 Subject: [PATCH 13/24] Refactor --- .../com/networknt/schema/RefValidator.java | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 8269dba7d..6cda3e5d7 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -54,26 +54,6 @@ public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, JsonNodePath evaluationPath) { - // $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) { - SchemaLocation schemaLocation = SchemaLocation.of(base.getCurrentUri().toString()); - JsonNodePath fragment = new JsonNodePath(PathType.JSON_POINTER); - if (refValue.startsWith("#")) { - schemaLocation = SchemaLocation.of(schemaLocation.getAbsoluteIri() + refValue); - } else { - schemaLocation = new SchemaLocation(schemaLocation.getAbsoluteIri().resolve(refValue), - fragment); - } - JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaLocation.toString()); - if (schemaResource != null) { - // Schema resource needs to update the parent and evaluation path - return new JsonSchemaRef(() -> schemaResource.fromRef(parentSchema, evaluationPath)); - } - } // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; @@ -124,6 +104,21 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return new JsonSchemaRef(parent); } refValue = refValue.substring(index); + } 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); + JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); + if (schemaResource != null) { + // Schema resource needs to update the parent and evaluation path + return new JsonSchemaRef( + new CachedSupplier<>(() -> schemaResource.fromRef(parentSchema, evaluationPath))); + } + } } if (refValue.equals(REF_CURRENT)) { Supplier supplier = parent; From a71356cee7d2112bd004f78ca31bb61355cd2ba2 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 8 Jan 2024 14:03:44 +0800 Subject: [PATCH 14/24] Refactor --- .../java/com/networknt/schema/JsonSchema.java | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index ad870e2e6..e7c62e4be 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -80,15 +80,7 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc if (uriRefersToSubschema(currentUri, schemaLocation)) { updateThisAsSubschema(currentUri); } - 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); - } - } - } + initializeConfig(); this.id = validationContext.resolveSchemaId(this.schemaNode); this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); readDefinitions("definitions"); @@ -101,6 +93,20 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString() + "#" + anchor, this); } } + + 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); + } + } + } + } /** * Copy constructor. @@ -142,6 +148,7 @@ public JsonSchema fromRef(JsonSchema refParent, JsonNodePath refEvaluationPath) copy.requiredValidator = null; copy.typeValidator = null; copy.validators = null; + copy.initializeConfig(); return copy; } From f1afef5164b04e3074cc1e71bb800f30dc3e7b1b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:43:05 +0800 Subject: [PATCH 15/24] Refactor shift subschema loading to factory --- .../java/com/networknt/schema/JsonSchema.java | 35 ------------------ .../networknt/schema/JsonSchemaFactory.java | 36 ++++++++----------- 2 files changed, 14 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index e7c62e4be..0b99e2a38 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -21,13 +21,11 @@ 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.*; @@ -76,10 +74,6 @@ 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); @@ -187,35 +181,6 @@ private static boolean isUriFragmentWithNoContext(URI currentUri, String id) { return id.startsWith("#") && (currentUri == null || currentUri.toString().startsWith("#")); } - 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); - } - public URI getCurrentUri() { return this.currentUri; } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 0880ed27d..2a13d50f2 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -463,35 +463,27 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig 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); + 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 { - String schemaLocationValue = schemaUri.toString(); - String id = jsonMetaSchema.readId(schemaNode); - if (id != null) { - schemaLocationValue = id; - } - if (schemaLocationValue.contains("#")) { - // Schema location needs to strip off the fragment as the json schema for the constructor to detect - // it is a subschema - schemaLocationValue = schemaLocationValue.substring(0, schemaLocationValue.indexOf("#") + 1); - } - if (!schemaLocationValue.contains("#")) { - schemaLocationValue = schemaLocationValue + "#"; - } - SchemaLocation schemaLocation = SchemaLocation.of(schemaLocationValue); + // Subschema final ValidationContext validationContext = createValidationContext(schemaNode); validationContext.setConfig(config); - jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, false); + 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 { + throw new JsonSchemaException("Unable to find subschema"); + } } return jsonSchema; - } catch (IOException e) { + } catch (IOException | URISyntaxException e) { logger.error("Failed to load json schema!", e); throw new JsonSchemaException(e); } From 5e2b4909ee1ffd3cde461ec5b2295534c820a6ed Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Fri, 12 Jan 2024 08:58:30 +0800 Subject: [PATCH 16/24] Fix ref --- src/main/java/com/networknt/schema/BaseJsonValidator.java | 7 +++++++ src/main/java/com/networknt/schema/JsonSchema.java | 8 ++++---- .../com/networknt/schema/ValidationMessageHandler.java | 2 ++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 24b751dee..b662afece 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -263,6 +263,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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 0b99e2a38..dba33ba89 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -128,15 +128,15 @@ protected JsonSchema(JsonSchema copy) { * This is typically used if this schema is a schema resource that can be * pointed to by various references. * - * @param refParent the parent ref + * @param refEvaluationParentSchema the parent ref * @param refEvaluationPath the ref evaluation path * @return the schema */ - public JsonSchema fromRef(JsonSchema refParent, JsonNodePath refEvaluationPath) { + public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) { JsonSchema copy = new JsonSchema(this); - copy.validationContext.setConfig(refParent.validationContext.getConfig()); + copy.validationContext.setConfig(refEvaluationParentSchema.validationContext.getConfig()); copy.evaluationPath = refEvaluationPath; - copy.parentSchema = refParent; + copy.evaluationParentSchema = refEvaluationParentSchema; // Validator state is reset due to the changes in evaluation path copy.validatorsLoaded = false; copy.requiredValidator = null; diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 2510ca9fc..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; @@ -49,6 +50,7 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { 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; From 2c37d67bc539b11d49d099412fb2bb2f093ba3c9 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:39:15 +0800 Subject: [PATCH 17/24] Refactor ref --- .../com/networknt/schema/RefValidator.java | 150 ++++++++++-------- .../networknt/schema/ValidationContext.java | 4 +- 2 files changed, 83 insertions(+), 71 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 6cda3e5d7..4efcae23e 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -26,7 +26,6 @@ import java.net.URI; import java.util.*; -import java.util.function.Supplier; public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); @@ -57,7 +56,6 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val // The evaluationPath is used to derive the keywordLocation final String refValueOriginal = refValue; - Supplier 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; @@ -70,7 +68,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.get(), refUri); + URI schemaUri = determineSchemaUri(validationContext.getURIFactory(), parentSchema, refUri); if (schemaUri == null) { // the URNFactory is optional if (validationContext.getURNFactory() == null) { @@ -83,27 +81,27 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } } else if (URN_SCHEME.equals(schemaUri.getScheme())) { // Try to resolve URN schema as a JsonSchemaRef to some sub-schema of the parent - JsonSchemaRef ref = getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); + JsonSchema ref = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); if (ref != null) { - return ref; + return new JsonSchemaRef(() -> ref); } } URI schemaUriFinal = schemaUri; // This should retrieve schemas regardless of the protocol that is in the uri. - parent = new CachedSupplier<>(() -> { + return new JsonSchemaRef(new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); - if (schemaResource != null) { - // Schema resource needs to update the parent and evaluation path - return schemaResource.fromRef(parentSchema, evaluationPath); + if (schemaResource == null) { + schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()); } - return validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()) - .findAncestor().fromRef(parentSchema, evaluationPath); // Setting the parent affects the lexical root - }); - if (index < 0) { - return new JsonSchemaRef(parent); - } - refValue = refValue.substring(index); + if (index < 0) { + return schemaResource; + } else { + String newRefValue = refValue.substring(index); + return getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, evaluationPath); + } + })); + } else if (SchemaLocation.Fragment.isAnchorFragment(refValue)) { // $ref prevents a sibling $id from changing the base uri JsonSchema base = parentSchema; @@ -121,69 +119,75 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } } if (refValue.equals(REF_CURRENT)) { - Supplier supplier = parent; - return new JsonSchemaRef(() -> supplier.get().findSchemaResourceRoot()); + return new JsonSchemaRef(() -> parentSchema.findSchemaResourceRoot()); } - return getJsonSchemaRef(parent, validationContext, refValue, refValueOriginal, evaluationPath); + return new JsonSchemaRef(() -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath)); } - private static JsonSchemaRef getJsonSchemaRef(Supplier parentSupplier, + private static JsonSchema getJsonSchema(JsonSchema parent, ValidationContext validationContext, String refValue, String refValueOriginal, JsonNodePath evaluationPath) { - JsonSchema parent = parentSupplier.get(); JsonNode node = parent.getRefSchemaNode(refValue); - if (node != null) { return validationContext.getSchemaReferences().computeIfAbsent(refValueOriginal, key -> { - 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); - } + 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); } } - 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]); - } } - final JsonSchema schema = validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); - return new JsonSchemaRef(() -> schema); - }); + 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 validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); } - return null; + throw new JsonSchemaException("Cannot find ref "+refValue); } private static URI determineSchemaUri(final URIFactory uriFactory, final JsonSchema parentSchema, final String refUri) { @@ -225,7 +229,11 @@ public Set validate(ExecutionContext executionContext, JsonNo // 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()); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + throw new JsonSchemaException("Unable to resolve ref"); + } + refSchema.getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); if (this.schema != null) { errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); } else { @@ -252,9 +260,13 @@ 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) { + throw new JsonSchemaException("Unable to resolve ref"); + } + refSchema.getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); + if (refSchema != null) { + errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } return errors; } finally { diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 80633329e..1efda172a 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -37,7 +37,7 @@ public class ValidationContext { private final JsonSchemaFactory jsonSchemaFactory; private SchemaValidatorsConfig config; private final Stack discriminatorContexts = new Stack<>(); - private final ConcurrentMap schemaReferences = new ConcurrentHashMap<>(); + private final ConcurrentMap schemaReferences = new ConcurrentHashMap<>(); private final ConcurrentMap schemaResources = new ConcurrentHashMap<>(); public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema, @@ -103,7 +103,7 @@ public void setConfig(SchemaValidatorsConfig config) { * * @return the schema references */ - public ConcurrentMap getSchemaReferences() { + public ConcurrentMap getSchemaReferences() { return this.schemaReferences; } From 6731f7a1d1d95fd978276333f898b6217773dc7e Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:26:05 +0800 Subject: [PATCH 18/24] Refactor discriminator --- .../com/networknt/schema/AllOfValidator.java | 2 +- .../com/networknt/schema/AnyOfValidator.java | 8 +-- .../networknt/schema/BaseJsonValidator.java | 3 +- .../schema/DiscriminatorContext.java | 41 ++++++++++++++ .../networknt/schema/ExecutionContext.java | 18 ++++++ .../java/com/networknt/schema/JsonSchema.java | 18 +++--- .../networknt/schema/ValidationContext.java | 55 ------------------- 7 files changed, 74 insertions(+), 71 deletions(-) create mode 100644 src/main/java/com/networknt/schema/DiscriminatorContext.java diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index a2a80df47..1078c9d50 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -74,7 +74,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 3c5a725a0..403971e21 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; } @@ -62,7 +62,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(); @@ -153,7 +153,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 b662afece..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; @@ -126,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) { 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/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/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index dba33ba89..caf4740ba 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -20,7 +20,6 @@ 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.walk.DefaultKeywordWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; @@ -92,13 +91,6 @@ 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); - } - } } } @@ -431,6 +423,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. @@ -469,7 +469,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/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 1efda172a..982fe1cc4 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -17,15 +17,11 @@ package com.networknt.schema; import java.net.URI; -import java.util.HashMap; -import java.util.Map; 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; @@ -36,7 +32,6 @@ public class ValidationContext { private final JsonMetaSchema metaSchema; private final JsonSchemaFactory jsonSchemaFactory; private SchemaValidatorsConfig config; - private final Stack discriminatorContexts = new Stack<>(); private final ConcurrentMap schemaReferences = new ConcurrentHashMap<>(); private final ConcurrentMap schemaResources = new ConcurrentHashMap<>(); @@ -116,21 +111,6 @@ public ConcurrentMap getSchemaResources() { return this.schemaResources; } - 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(); - } - public JsonMetaSchema getMetaSchema() { return this.metaSchema; } @@ -139,39 +119,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(); - } - } } From 85e8a8985769d1b09b19ec4123524d1fc8100b9c Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:14:04 +0800 Subject: [PATCH 19/24] Refactor --- .../com/networknt/schema/RefValidator.java | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 4efcae23e..2115b4e26 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -19,7 +19,6 @@ 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; @@ -35,7 +34,6 @@ public class RefValidator extends BaseJsonValidator { 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); @@ -79,12 +77,6 @@ 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 - JsonSchema ref = getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath); - if (ref != null) { - return new JsonSchemaRef(() -> ref); - } } URI schemaUriFinal = schemaUri; @@ -95,10 +87,15 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()); } if (index < 0) { - return schemaResource; + return schemaResource.fromRef(parentSchema, evaluationPath); } else { String newRefValue = refValue.substring(index); - return getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, evaluationPath); + schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, + evaluationPath); + if (schemaResource == null) { + throw new JsonSchemaException("Failed to resolve ref"); + } + return schemaResource.fromRef(parentSchema, evaluationPath); } })); @@ -119,9 +116,11 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } } if (refValue.equals(REF_CURRENT)) { - return new JsonSchemaRef(() -> parentSchema.findSchemaResourceRoot()); + return new JsonSchemaRef(new CachedSupplier<>( + () -> parentSchema.findSchemaResourceRoot().fromRef(parentSchema, evaluationPath))); } - return new JsonSchemaRef(() -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath)); + return new JsonSchemaRef(new CachedSupplier<>( + () -> getJsonSchema(parentSchema, validationContext, refValue, refValueOriginal, evaluationPath))); } private static JsonSchema getJsonSchema(JsonSchema parent, From 57667ef04926d99c1a7a0c076e604f270d31812b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 16 Jan 2024 16:16:08 +0800 Subject: [PATCH 20/24] Refactor validation context --- .../java/com/networknt/schema/JsonSchema.java | 24 ++++++++- .../networknt/schema/JsonSchemaFactory.java | 18 ++----- .../schema/RecursiveRefValidator.java | 20 +++---- .../com/networknt/schema/RefValidator.java | 4 -- .../networknt/schema/ValidationContext.java | 26 ++++----- .../networknt/schema/SharedConfigTest.java | 54 +++++++++++++++++++ 6 files changed, 106 insertions(+), 40 deletions(-) create mode 100644 src/test/java/com/networknt/schema/SharedConfigTest.java diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index caf4740ba..f7c1dce80 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -126,7 +126,11 @@ protected JsonSchema(JsonSchema copy) { */ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath refEvaluationPath) { JsonSchema copy = new JsonSchema(this); - copy.validationContext.setConfig(refEvaluationParentSchema.validationContext.getConfig()); + 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 @@ -138,6 +142,24 @@ public JsonSchema fromRef(JsonSchema refEvaluationParentSchema, JsonNodePath ref 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; + } + public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { return getValidationContext().newSchema(schemaLocation, evaluationPath, schemaNode, this); } diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index 2a13d50f2..db839349a 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -325,8 +325,7 @@ 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); } @@ -362,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) { @@ -440,13 +439,7 @@ public JsonSchema getSchema(final URI schemaUri, final SchemaValidatorsConfig co JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(mappedUri, key -> { return getMappedSchema(schemaUri, config, 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; + return cachedUriSchema.withConfig(config); } return getMappedSchema(schemaUri, config, mappedUri); } @@ -470,8 +463,7 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig jsonSchema = doCreate(validationContext, schemaLocation, evaluationPath, mappedUri, schemaNode, null, true /* retrieved via id, resolving will not change anything */); } else { // Subschema - final ValidationContext validationContext = createValidationContext(schemaNode); - validationContext.setConfig(config); + 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); 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 2115b4e26..d71f99a03 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -31,8 +31,6 @@ public class RefValidator extends BaseJsonValidator { protected JsonSchemaRef schema; - private JsonSchema parentSchema; - private static final String REF_CURRENT = "#"; public RefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { @@ -232,7 +230,6 @@ public Set validate(ExecutionContext executionContext, JsonNo if (refSchema == null) { throw new JsonSchemaException("Unable to resolve ref"); } - refSchema.getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); if (this.schema != null) { errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); } else { @@ -263,7 +260,6 @@ public Set walk(ExecutionContext executionContext, JsonNode n if (refSchema == null) { throw new JsonSchemaException("Unable to resolve ref"); } - refSchema.getValidationContext().setConfig(this.parentSchema.getValidationContext().getConfig()); if (refSchema != null) { errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java index 982fe1cc4..1e465b05f 100644 --- a/src/main/java/com/networknt/schema/ValidationContext.java +++ b/src/main/java/com/networknt/schema/ValidationContext.java @@ -31,12 +31,19 @@ public class ValidationContext { private final URNFactory urnFactory; private final JsonMetaSchema metaSchema; private final JsonSchemaFactory jsonSchemaFactory; - private SchemaValidatorsConfig config; - private final ConcurrentMap schemaReferences = new ConcurrentHashMap<>(); - private final ConcurrentMap schemaResources = new ConcurrentHashMap<>(); + 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"); } @@ -50,7 +57,9 @@ 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) { @@ -83,16 +92,9 @@ 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; - } - /** * Gets the schema references identified by the ref uri. * 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 From 155fa2424285f6efce49adfd8b45246bc4a1b31b Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Tue, 16 Jan 2024 14:56:52 +0800 Subject: [PATCH 21/24] Load validators in constructor --- .../com/networknt/schema/AllOfValidator.java | 5 ---- .../com/networknt/schema/AnyOfValidator.java | 5 ---- .../java/com/networknt/schema/JsonSchema.java | 29 +------------------ .../schema/NonValidationKeyword.java | 9 ++++++ .../com/networknt/schema/NotValidator.java | 4 --- .../com/networknt/schema/OneOfValidator.java | 5 ---- .../schema/PrefixItemsValidatorTest.java | 10 ++----- 7 files changed, 13 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index 1078c9d50..415e8e958 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -38,11 +38,6 @@ public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath this.schemas.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), schemaNode.get(i), parentSchema)); } - for (JsonSchema schema : this.schemas) { - // Load the validators to parse the schema so that schema resources with $id can - // be identified - schema.getValidators(); - } } @Override diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 403971e21..f3828cc1d 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -46,11 +46,6 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath } else { this.discriminatorContext = null; } - for (JsonSchema schema : this.schemas) { - // Load the validators to parse the schema so that schema resources with $id can - // be identified - schema.getValidators(); - } } @Override diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index f7c1dce80..bdf2d4763 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -76,8 +76,6 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc initializeConfig(); this.id = validationContext.resolveSchemaId(this.schemaNode); this.anchor = validationContext.getMetaSchema().readAnchor(this.schemaNode); - readDefinitions("definitions"); - readDefinitions("$defs"); if (this.id != null) { this.validationContext.getSchemaResources() .putIfAbsent(this.currentUri != null ? this.currentUri.toString() : this.id, this); @@ -85,6 +83,7 @@ private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLoc if (this.anchor != null) { this.validationContext.getSchemaResources().putIfAbsent(this.currentUri.toString() + "#" + anchor, this); } + getValidators(); } private void initializeConfig() { @@ -160,10 +159,6 @@ public JsonSchema withConfig(SchemaValidatorsConfig config) { return this; } - public JsonSchema createChildSchema(SchemaLocation schemaLocation, JsonNode schemaNode) { - return getValidationContext().newSchema(schemaLocation, evaluationPath, schemaNode, this); - } - ValidationContext getValidationContext() { return this.validationContext; } @@ -318,28 +313,6 @@ private JsonNode handleNullNode(String ref, JsonSchema schema) { return null; } - private void readDefinitions(String definitionsKeyword) { - JsonNode definitionsNode = schemaNode.get(definitionsKeyword); - if (definitionsNode != null) { - readSchemaResources(definitionsNode, this.schemaLocation.append(definitionsKeyword), - this.evaluationPath.append(definitionsKeyword)); - } - } - - private void readSchemaResources(JsonNode definitionsNode, SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - Iterator pnames = definitionsNode.fieldNames(); - while (pnames.hasNext()) { - String pname = pnames.next(); - JsonNode nodeToUse = definitionsNode.get(pname); - // The schema resources with id or anchor will be stored during the constructor - // call of JsonSchema - JsonSchema schema = this.validationContext.newSchema(schemaLocation.append(pname), evaluationPath.append(pname), nodeToUse, - this); - schema.getValidators(); - } - } - - /** * Please note that the key in {@link #validators} map is the evaluation path. */ diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index b669572c5..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; /** @@ -36,6 +38,13 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso // 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 diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 2e00b530d..6e5164301 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -32,10 +32,6 @@ public class NotValidator extends BaseJsonValidator { public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext); this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); - - // Load the validators to parse the schema so that schema resources with $id can - // be identified - this.schema.getValidators(); } @Override diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index ffaf95b12..e7e8eb7b4 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -36,11 +36,6 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath JsonNode childNode = schemaNode.get(i); this.schemas.add(validationContext.newSchema( schemaLocation.append(i), evaluationPath.append(i), childNode, parentSchema)); } - for (JsonSchema schema : this.schemas) { - // Load the validators to parse the schema so that schema resources with $id can - // be identified - schema.getValidators(); - } } @Override 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 -> { + }); }); } ); From 9d31335f6a5eac677dff4455ed3f0fe9041fa866 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 17 Jan 2024 11:08:02 +0800 Subject: [PATCH 22/24] Schema location --- .../java/com/networknt/schema/AbsoluteIri.java | 15 ++++++++++++--- .../com/networknt/schema/AbsoluteIriTest.java | 6 ++++++ 2 files changed, 18 insertions(+), 3 deletions(-) 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/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"); From 3d7c17505c5e22083ebdd6c86dc4c4b637939164 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:56:39 +0800 Subject: [PATCH 23/24] Refactor ref validator --- .../networknt/schema/JsonSchemaFactory.java | 2 +- .../com/networknt/schema/JsonSchemaRef.java | 13 ---- .../com/networknt/schema/RefValidator.java | 77 +++++++++++-------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index db839349a..03d7cf5b9 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -476,7 +476,7 @@ protected JsonSchema getMappedSchema(final URI schemaUri, SchemaValidatorsConfig } return jsonSchema; } catch (IOException | URISyntaxException e) { - logger.error("Failed to load json schema!", 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 f4ebdd7b6..c4d4cf4ca 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaRef.java +++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java @@ -15,9 +15,6 @@ */ package com.networknt.schema; -import com.fasterxml.jackson.databind.JsonNode; - -import java.util.Set; import java.util.function.Supplier; /** @@ -31,17 +28,7 @@ public JsonSchemaRef(Supplier schema) { this.schemaSupplier = schema; } - public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation) { - return getSchema().validate(executionContext, node, rootNode, instanceLocation); - } - public JsonSchema getSchema() { return this.schemaSupplier.get(); } - - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - return getSchema().walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); - } } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index d71f99a03..1e5c74ee5 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -24,7 +24,9 @@ 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); @@ -36,15 +38,7 @@ public class RefValidator extends BaseJsonValidator { 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, @@ -82,16 +76,29 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val return new JsonSchemaRef(new CachedSupplier<>(() -> { JsonSchema schemaResource = validationContext.getSchemaResources().get(schemaUriFinal.toString()); if (schemaResource == null) { - schemaResource = validationContext.getJsonSchemaFactory().getSchema(schemaUriFinal, validationContext.getConfig()); + 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) { - throw new JsonSchemaException("Failed to resolve ref"); + return null; } return schemaResource.fromRef(parentSchema, evaluationPath); } @@ -105,12 +112,17 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val } if (base.getCurrentUri() != null) { String absoluteIri = SchemaLocation.resolve(base.getSchemaLocation(), refValue); - JsonSchema schemaResource = validationContext.getSchemaResources().get(absoluteIri); - if (schemaResource != null) { - // Schema resource needs to update the parent and evaluation path - return new JsonSchemaRef( - new CachedSupplier<>(() -> schemaResource.fromRef(parentSchema, evaluationPath))); - } + // 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); + })); } } if (refValue.equals(REF_CURRENT)) { @@ -184,7 +196,7 @@ private static JsonSchema getJsonSchema(JsonNode node, JsonSchema parent, } return validationContext.newSchema(path, evaluationPath, currentUri, node, currentParent); } - throw new JsonSchemaException("Cannot find ref "+refValue); + throw null; } private static URI determineSchemaUri(final URIFactory uriFactory, final JsonSchema parentSchema, final String refUri) { @@ -218,23 +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. JsonSchema refSchema = this.schema.getSchema(); if (refSchema == null) { - throw new JsonSchemaException("Unable to resolve ref"); - } - if (this.schema != null) { - errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); - } else { - errors = Collections.emptySet(); + 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()) { @@ -248,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 { @@ -258,11 +267,13 @@ public Set walk(ExecutionContext executionContext, JsonNode n // with the latest config. Reset the config. JsonSchema refSchema = this.schema.getSchema(); if (refSchema == null) { - throw new JsonSchemaException("Unable to resolve ref"); - } - if (refSchema != null) { - errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + 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(); From afe9d784b021e70c6ac39079f3c0eaa5109c2122 Mon Sep 17 00:00:00 2001 From: Justin Tay <49700559+justin-tay@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:02:17 +0800 Subject: [PATCH 24/24] Fix enum --- .../com/networknt/schema/EnumValidator.java | 60 +++++++++++- src/test/suite/tests/draft-next/enum.json | 96 +++++++++++++++++++ src/test/suite/tests/draft2019-09/enum.json | 96 +++++++++++++++++++ src/test/suite/tests/draft2020-12/enum.json | 96 +++++++++++++++++++ src/test/suite/tests/draft4/enum.json | 84 ++++++++++++++++ src/test/suite/tests/draft6/enum.json | 84 ++++++++++++++++ src/test/suite/tests/draft7/enum.json | 84 ++++++++++++++++ 7 files changed, 597 insertions(+), 3 deletions(-) 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/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/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/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/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/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" ] },