diff options
author | Justin Tay <49700559+justin-tay@users.noreply.github.com> | 2024-03-12 20:55:14 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-12 08:55:14 -0400 |
commit | 0f983b05ed7478c55db1dc0d820f60f0becaf787 (patch) | |
tree | dba6e09a32f66904895afdf9db35dfee52470cfa | |
parent | eea61d6b630ee1277371bf7ffc0e171ac4cbff54 (diff) | |
download | json-schema-validator-0f983b05ed7478c55db1dc0d820f60f0becaf787.tar.gz |
Refactor walk (#986)
* Allow apply defaults to resolve ref and refactor walk listeners
* Refactor
* Refactor
* Refactor
* Refactor
* Refactor
* Refactor
* Refactor
* Update docs
* Refactor
* Refactor
* Refactor
* Add test
28 files changed, 1250 insertions, 298 deletions
diff --git a/doc/upgrading.md b/doc/upgrading.md index a72ad22..506e967 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -6,7 +6,30 @@ This contains information on the notable or breaking changes in each version. ### 1.4.0 -This contains breaking changes in how custom meta-schemas are created. +This contains breaking changes +- to those using the walk functionality +- in how custom meta-schemas are created + +When using the walker with defaults the `default` across a `$ref` are properly resolved and used. + +The behavior for the property listener is now more consistent whether or not validation is enabled. Previously if validation is enabled but the property is `null` the property listener is not called while if validation is not enabled it will be called. Now the property listener will be called in both scenarios. + +The following are the breaking changes to those using the walk functionality. + +`WalkEvent` +| Field | Change | Notes +|--------------------------|--------------|---------- +| `schemaLocation` | Removed | For keywords: `getValidator().getSchemaLocation()`. For items and properties: `getSchema().getSchemaLocation()` +| `evaluationPath` | Removed | For keywords: `getValidator().getEvaluationPath()`. For items and properties: `getSchema().getEvaluationPath()` +| `schemaNode` | Removed | `getSchema().getSchemaNode()` +| `parentSchema` | Removed | `getSchema().getParentSchema()` +| `schema` | New | For keywords this is the parent schema of the validator. For items and properties this is the item or property schema being evaluated. +| `node` | Renamed | `instanceNode` +| `currentJsonSchemaFactory`| Removed | `getSchema().getValidationContext().getJsonSchemaFactory()` +| `validator` | New | The validator indicated by the keyword. + + +The following are the breaking changes in how custom meta-schemas are created. `JsonSchemaFactory` * The following were renamed on `JsonSchemaFactory` builder diff --git a/doc/walkers.md b/doc/walkers.md index b06fb12..51736e4 100644 --- a/doc/walkers.md +++ b/doc/walkers.md @@ -38,65 +38,61 @@ public interface JsonSchemaWalker { The JSONValidator interface extends this new interface thus allowing all the validator's defined in library to implement this new interface. BaseJsonValidator class provides a default implementation of the walk method. In this case the walk method does nothing but validating based on shouldValidateSchema parameter. ```java -/** + /** * This is default implementation of walk method. Its job is to call the * validate method if shouldValidateSchema is enabled. */ @Override - public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema); - Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>(); - if (shouldValidateSchema) { - validationMessages = validate(executionContext, node, rootNode, instanceLocation); - } - return validationMessages; + default Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) + : Collections.emptySet(); } - ``` A new walk method added to the JSONSchema class allows us to walk through the JSONSchema. ```java - public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { - // Create the collector context object. - CollectorContext collectorContext = new CollectorContext(); - // Set the collector context in thread info, this is unique for every thread. - ThreadInfo.set(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, collectorContext); - Set<ValidationMessage> errors = walk(node, node, AT_ROOT, shouldValidateSchema); - // Load all the data from collectors into the context. - collectorContext.loadCollectors(); - // Collect errors and collector context into validation result. - ValidationResult validationResult = new ValidationResult(errors, collectorContext); - return validationResult; + public ValidationResult walk(JsonNode node, boolean validate) { + return walk(createExecutionContext(), node, validate); } @Override - public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { - Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>(); + public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + Set<ValidationMessage> errors = new LinkedHashSet<>(); // Walk through all the JSONWalker's. - for (Entry<String, JsonValidator> entry : validators.entrySet()) { - JsonWalker jsonWalker = entry.getValue(); - String schemaPathWithKeyword = entry.getKey(); + for (JsonValidator validator : getValidators()) { + JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath(); try { - // Call all the pre-walk listeners. If all the pre-walk listeners return true - // then continue to walk method. - if (keywordWalkListenerRunner.runPreWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath, - schemaNode, parentSchema)) { - validationMessages.addAll(jsonWalker.walk(node, rootNode, at, shouldValidateSchema)); + // Call all the pre-walk listeners. If at least one of the pre walk listeners + // returns SKIP, then skip the walk. + if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + this, validator)) { + Set<ValidationMessage> results = null; + try { + results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + } finally { + if (results != null && !results.isEmpty()) { + errors.addAll(results); + } + } } } finally { // Call all the post-walk listeners. - keywordWalkListenerRunner.runPostWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath, - schemaNode, parentSchema, validationMessages); + this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + this, validator, errors); } } - return validationMessages; + return errors; } ``` Following code snippet shows how to call the walk method on a JsonSchema instance. -``` -ValidationResult result = jsonSchema.walk(data,false); +```java +ValidationResult result = jsonSchema.walk(data, false); ``` @@ -122,7 +118,7 @@ private static class PropertiesKeywordListener implements JsonSchemaWalkListener @Override public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { - JsonNode schemaNode = keywordWalkEvent.getSchemaNode(); + JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); if (schemaNode.get("title").textValue().equals("Property3")) { return WalkFlow.SKIP; } @@ -146,7 +142,7 @@ SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new PropertiesKeywordListener()); final JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema) + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) .build(); this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig); @@ -160,7 +156,7 @@ Both property walk listeners and keyword walk listener can be modeled by using t SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); schemaValidatorsConfig.addPropertyWalkListener(new ExamplePropertyWalkListener()); final JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema) + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) .build(); this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig); @@ -176,16 +172,15 @@ Following snippet shows the details captured by WalkEvent instance. ```java public class WalkEvent { - private ExecutionContext executionContext; - private SchemaLocation schemaLocation; - private JsonNodePath evaluationPath; - private JsonNode schemaNode; - private JsonSchema parentSchema; + private JsonSchema schema; private String keyword; - private JsonNode node; private JsonNode rootNode; + private JsonNode instanceNode; private JsonNodePath instanceLocation; + private JsonValidator validator; + ... +} ``` ### Sample Flow @@ -285,7 +280,7 @@ But if we apply defaults while walking, then required validation passes, and the JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); schemaValidatorsConfig.setApplyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)); - JsonSchema jsonSchema = schemaFactory.getSchema(getClass().getClassLoader().getResourceAsStream("schema.json"), schemaValidatorsConfig); + JsonSchema jsonSchema = schemaFactory.getSchema(SchemaLocation.of("classpath:schema.json"), schemaValidatorsConfig); JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data.json")); ValidationResult result = jsonSchema.walk(inputNode, true); diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index a06f16c..291144b 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -27,11 +27,10 @@ import java.util.Set; */ public class ConstValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(ConstValidator.class); - JsonNode schemaNode; - public ConstValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { + public ConstValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONST, validationContext); - this.schemaNode = schemaNode; } public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index a113d11..6f72993 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -28,7 +28,7 @@ import java.util.*; public class DynamicRefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(DynamicRefValidator.class);
- protected JsonSchemaRef schema;
+ protected final JsonSchemaRef schema;
public DynamicRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DYNAMIC_REF, validationContext);
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 1905886..7aede16 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -19,6 +19,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.JsonSchemaRefs; import com.networknt.schema.utils.SetView; import org.slf4j.Logger; @@ -35,7 +36,7 @@ public class ItemsValidator extends BaseJsonValidator { private final JsonSchema schema; private final List<JsonSchema> tupleSchema; - private Boolean additionalItems; + private final Boolean additionalItems; private final JsonSchema additionalSchema; private Boolean hasUnevaluatedItemsValidator = null; @@ -47,6 +48,8 @@ public class ItemsValidator extends BaseJsonValidator { public ItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext); + Boolean additionalItems = null; + this.tupleSchema = new ArrayList<>(); JsonSchema foundSchema = null; JsonSchema foundAdditionalSchema = null; @@ -66,7 +69,7 @@ public class ItemsValidator extends BaseJsonValidator { if (addItemNode != null) { additionalItemsSchemaNode = addItemNode; if (addItemNode.isBoolean()) { - this.additionalItems = addItemNode.asBoolean(); + additionalItems = addItemNode.asBoolean(); } else if (addItemNode.isObject()) { foundAdditionalSchema = validationContext.newSchema( parentSchema.schemaLocation.append(PROPERTY_ADDITIONAL_ITEMS), @@ -74,6 +77,7 @@ public class ItemsValidator extends BaseJsonValidator { } } } + this.additionalItems = additionalItems; this.schema = foundSchema; this.additionalSchema = foundAdditionalSchema; this.additionalItemsEvaluationPath = parentSchema.evaluationPath.append(PROPERTY_ADDITIONAL_ITEMS); @@ -205,7 +209,7 @@ public class ItemsValidator extends BaseJsonValidator { JsonNode defaultNode = null; if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults() && this.schema != null) { - defaultNode = this.schema.getSchemaNode().get("default"); + defaultNode = getDefaultNode(this.schema); } int i = 0; for (JsonNode n : arrayNode) { @@ -222,6 +226,17 @@ public class ItemsValidator extends BaseJsonValidator { return validationMessages; } + private static JsonNode getDefaultNode(JsonSchema schema) { + JsonNode result = schema.getSchemaNode().get("default"); + if (result == null) { + JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema); + if (schemaRef != null) { + result = getDefaultNode(schemaRef.getSchema()); + } + } + return result; + } + private void doWalk(ExecutionContext executionContext, HashSet<ValidationMessage> validationMessages, int i, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { if (this.schema != null) { @@ -247,14 +262,12 @@ public class ItemsValidator extends BaseJsonValidator { private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema, Set<ValidationMessage> validationMessages) { boolean executeWalk = this.validationContext.getConfig().getItemWalkListenerRunner().runPreWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(), - node, rootNode, instanceLocation, walkSchema.getEvaluationPath(), walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory()); + node, rootNode, instanceLocation, walkSchema, this); if (executeWalk) { validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); } this.validationContext.getConfig().getItemWalkListenerRunner().runPostWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(), node, rootNode, - instanceLocation, this.evaluationPath, walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages); + instanceLocation, walkSchema, this, validationMessages); } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 196d474..a9cd02a 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -19,6 +19,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.JsonSchemaRefs; import com.networknt.schema.utils.SetView; import org.slf4j.Logger; @@ -119,7 +120,7 @@ public class ItemsValidator202012 extends BaseJsonValidator { JsonNode defaultNode = null; if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults() && this.schema != null) { - defaultNode = this.schema.getSchemaNode().get("default"); + defaultNode = getDefaultNode(this.schema); } for (int i = this.prefixCount; i < node.size(); ++i) { JsonNode n = node.get(i); @@ -139,6 +140,17 @@ public class ItemsValidator202012 extends BaseJsonValidator { return validationMessages; } + private static JsonNode getDefaultNode(JsonSchema schema) { + JsonNode result = schema.getSchemaNode().get("default"); + if (result == null) { + JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema); + if (schemaRef != null) { + result = getDefaultNode(schemaRef.getSchema()); + } + } + return result; + } + private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema, Set<ValidationMessage> validationMessages) { //@formatter:off @@ -148,10 +160,7 @@ public class ItemsValidator202012 extends BaseJsonValidator { node, rootNode, instanceLocation, - walkSchema.getEvaluationPath(), - walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory() + walkSchema, this ); if (executeWalk) { validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); @@ -162,11 +171,8 @@ public class ItemsValidator202012 extends BaseJsonValidator { node, rootNode, instanceLocation, - this.evaluationPath, - walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), - this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages + walkSchema, + this, validationMessages ); //@formatter:on } diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 6c5f1d3..74ed724 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -22,11 +22,11 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.serialization.YamlMapperFactory; +import com.networknt.schema.utils.JsonNodes; import com.networknt.schema.utils.SetView; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -211,7 +211,7 @@ public class JsonSchema extends BaseJsonValidator { return this; } - ValidationContext getValidationContext() { + public ValidationContext getValidationContext() { return this.validationContext; } @@ -282,7 +282,7 @@ public class JsonSchema extends BaseJsonValidator { SchemaLocation schemaLocation = document.getSchemaLocation(); JsonNodePath evaluationPath = document.getEvaluationPath(); int nameCount = fragment.getNameCount(); - for (int x = 0; x < fragment.getNameCount(); x++) { + for (int x = 0; x < nameCount; x++) { /* * The sub schema is created by iterating through the parents in order to * maintain the lexical parent schema context. @@ -348,29 +348,9 @@ public class JsonSchema extends BaseJsonValidator { protected JsonNode getNode(Object propertyOrIndex) { return getNode(this.schemaNode, propertyOrIndex); } - + protected JsonNode getNode(JsonNode node, Object propertyOrIndex) { - JsonNode value = null; - if (propertyOrIndex instanceof Number) { - value = node.get(((Number) propertyOrIndex).intValue()); - } else { - // In the case of string this represents an escaped json pointer and thus does not reflect the property directly - String unescaped = propertyOrIndex.toString(); - if (unescaped.contains("~")) { - unescaped = unescaped.replace("~1", "/"); - unescaped = unescaped.replace("~0", "~"); - } - if (unescaped.contains("%")) { - try { - unescaped = URLDecoder.decode(unescaped, StandardCharsets.UTF_8.toString()); - } catch (UnsupportedEncodingException e) { - // Do nothing - } - } - - value = node.get(unescaped); - } - return value; + return JsonNodes.get(node, propertyOrIndex); } public JsonSchema findLexicalRoot() { @@ -563,9 +543,7 @@ public class JsonSchema extends BaseJsonValidator { try { results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); } finally { - if (results == null || results.isEmpty()) { - // Do nothing if valid - } else { + if (results != null && !results.isEmpty()) { if (errors == null) { errors = new SetView<>(); } @@ -918,22 +896,24 @@ public class JsonSchema extends BaseJsonValidator { * @return ValidationResult */ private ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode jsonNode, JsonNode rootNode, JsonNodePath instanceLocation) { - // Get the config. - SchemaValidatorsConfig config = this.validationContext.getConfig(); - // Get the collector context from the thread local. - CollectorContext collectorContext = executionContext.getCollectorContext(); // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); // Validate. Set<ValidationMessage> errors = validate(executionContext, jsonNode, rootNode, instanceLocation); + + // Get the config. + SchemaValidatorsConfig config = this.validationContext.getConfig(); + // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors. if (config.doLoadCollectors()) { + // Get the collector context. + CollectorContext collectorContext = executionContext.getCollectorContext(); + // Load all the data from collectors into the context. collectorContext.loadCollectors(); } // Collect errors and collector context into validation result. - ValidationResult validationResult = new ValidationResult(errors, executionContext); - return validationResult; + return new ValidationResult(errors, executionContext); } public ValidationResult validateAndCollect(JsonNode node) { @@ -1012,22 +992,22 @@ public class JsonSchema extends BaseJsonValidator { private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - // Get the config. - SchemaValidatorsConfig config = this.validationContext.getConfig(); - // Get the collector context. - CollectorContext collectorContext = executionContext.getCollectorContext(); // Set the walkEnabled flag in internal validator state. setValidatorState(executionContext, true, shouldValidateSchema); // Walk through the schema. Set<ValidationMessage> errors = walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + + // Get the config. + SchemaValidatorsConfig config = this.validationContext.getConfig(); // When walk is called in series of nested call we don't want to load the collectors every time. Leave to the API to decide when to call collectors. if (config.doLoadCollectors()) { + // Get the collector context. + CollectorContext collectorContext = executionContext.getCollectorContext(); + // Load all the data from collectors into the context. collectorContext.loadCollectors(); } - - ValidationResult validationResult = new ValidationResult(errors, executionContext); - return validationResult; + return new ValidationResult(errors, executionContext); } @Override @@ -1035,21 +1015,19 @@ public class JsonSchema extends BaseJsonValidator { JsonNodePath instanceLocation, boolean shouldValidateSchema) { Set<ValidationMessage> errors = new LinkedHashSet<>(); // Walk through all the JSONWalker's. - for (JsonValidator v : getValidators()) { - JsonNodePath evaluationPathWithKeyword = v.getEvaluationPath(); + for (JsonValidator validator : getValidators()) { + JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath(); try { // Call all the pre-walk listeners. If at least one of the pre walk listeners // returns SKIP, then skip the walk. if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, - v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode, - this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory())) { + this, validator)) { Set<ValidationMessage> results = null; try { - results = v.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } finally { - if (results == null || results.isEmpty()) { - } else { + if (results != null && !results.isEmpty()) { errors.addAll(results); } } @@ -1058,9 +1036,7 @@ public class JsonSchema extends BaseJsonValidator { // Call all the post-walk listeners. this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, - v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode, - this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory(), - errors); + this, validator, errors); } } return errors; diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index f23f303..0d33b12 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -29,13 +29,15 @@ import java.util.Set; public class MaxPropertiesValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MaxPropertiesValidator.class);
- private int max;
+ private final int max;
public MaxPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema,
ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MAX_PROPERTIES, validationContext);
if (schemaNode.canConvertToExactIntegral()) {
max = schemaNode.intValue();
+ } else {
+ max = Integer.MAX_VALUE;
}
}
diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index 26d6de1..1ff5664 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -29,13 +29,15 @@ import java.util.Set; public class MinPropertiesValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(MinPropertiesValidator.class);
- protected int min;
+ protected final int min;
public MinPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema,
ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MIN_PROPERTIES, validationContext);
if (schemaNode.canConvertToExactIntegral()) {
min = schemaNode.intValue();
+ } else {
+ min = 0;
}
}
diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index d798503..94bd136 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -19,6 +19,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.utils.JsonSchemaRefs; import com.networknt.schema.utils.SetView; import org.slf4j.Logger; @@ -107,7 +108,7 @@ public class PrefixItemsValidator extends BaseJsonValidator { int count = Math.min(node.size(), this.tupleSchema.size()); for (int i = 0; i < count; ++i) { JsonNode n = node.get(i); - JsonNode defaultNode = this.tupleSchema.get(i).getSchemaNode().get("default"); + JsonNode defaultNode = getDefaultNode(this.tupleSchema.get(i)); if (n.isNull() && defaultNode != null) { array.set(i, defaultNode); n = defaultNode; @@ -119,6 +120,17 @@ public class PrefixItemsValidator extends BaseJsonValidator { return validationMessages; } + private static JsonNode getDefaultNode(JsonSchema schema) { + JsonNode result = schema.getSchemaNode().get("default"); + if (result == null) { + JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema); + if (schemaRef != null) { + result = getDefaultNode(schemaRef.getSchema()); + } + } + return result; + } + private void doWalk(ExecutionContext executionContext, Set<ValidationMessage> validationMessages, int i, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, instanceLocation.append(i), @@ -134,10 +146,7 @@ public class PrefixItemsValidator extends BaseJsonValidator { node, rootNode, instanceLocation, - walkSchema.getEvaluationPath(), - walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory() + walkSchema, this ); if (executeWalk) { validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema)); @@ -148,11 +157,8 @@ public class PrefixItemsValidator extends BaseJsonValidator { node, rootNode, instanceLocation, - this.evaluationPath, - walkSchema.getSchemaLocation(), - walkSchema.getSchemaNode(), - walkSchema.getParentSchema(), - this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages + walkSchema, + this, validationMessages ); //@formatter:on } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index 45e055d..a47e61c 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.networknt.schema.annotation.JsonNodeAnnotation;
+import com.networknt.schema.utils.JsonSchemaRefs;
import com.networknt.schema.utils.SetView;
import com.networknt.schema.walk.WalkListenerRunner;
import org.slf4j.Logger;
@@ -98,7 +99,7 @@ public class PropertiesValidator extends BaseJsonValidator { if (errors == null) {
errors = new SetView<>();
}
- walkSchema(executionContext, entry, node, rootNode, instanceLocation, state.isValidationEnabled(), errors, this.validationContext.getConfig().getPropertyWalkListenerRunner());
+ walkSchema(executionContext, entry, node, rootNode, instanceLocation, state.isValidationEnabled(), errors, this.validationContext.getConfig().getPropertyWalkListenerRunner(), false);
}
// reset the complex flag to the original value before the recursive call
@@ -108,6 +109,18 @@ public class PropertiesValidator extends BaseJsonValidator { state.setMatchedNode(true);
}
} else {
+ if (state.isWalkEnabled()) {
+ // This tries to make the walk listener consistent between when validation is
+ // enabled or disabled as when validation is disabled it will walk where node is
+ // null.
+ // The actual walk needs to be skipped as the validators assume that node is not
+ // null.
+ if (errors == null) {
+ errors = new SetView<>();
+ }
+ walkSchema(executionContext, entry, node, rootNode, instanceLocation, state.isValidationEnabled(), errors, this.validationContext.getConfig().getPropertyWalkListenerRunner(), true);
+ }
+
// check whether the node which has not matched was mandatory or not
// the node was mandatory, decide which behavior to employ when validator has
@@ -156,7 +169,7 @@ public class PropertiesValidator extends BaseJsonValidator { } else {
WalkListenerRunner propertyWalkListenerRunner = this.validationContext.getConfig().getPropertyWalkListenerRunner();
for (Map.Entry<String, JsonSchema> entry : this.schemas.entrySet()) {
- walkSchema(executionContext, entry, node, rootNode, instanceLocation, shouldValidateSchema, validationMessages, propertyWalkListenerRunner);
+ walkSchema(executionContext, entry, node, rootNode, instanceLocation, shouldValidateSchema, validationMessages, propertyWalkListenerRunner, false);
}
}
return validationMessages;
@@ -177,7 +190,7 @@ public class PropertiesValidator extends BaseJsonValidator { for (Map.Entry<String, JsonSchema> entry : this.schemas.entrySet()) {
JsonNode propertyNode = node.get(entry.getKey());
- JsonNode defaultNode = getDefaultNode(entry);
+ JsonNode defaultNode = getDefaultNode(entry.getValue());
if (defaultNode == null) {
continue;
}
@@ -189,29 +202,37 @@ public class PropertiesValidator extends BaseJsonValidator { }
}
- private static JsonNode getDefaultNode(final Map.Entry<String, JsonSchema> entry) {
- JsonSchema propertySchema = entry.getValue();
- return propertySchema.getSchemaNode().get("default");
+ private static JsonNode getDefaultNode(JsonSchema schema) {
+ JsonNode result = schema.getSchemaNode().get("default");
+ if (result == null) {
+ JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema);
+ if (schemaRef != null) {
+ result = getDefaultNode(schemaRef.getSchema());
+ }
+ }
+ return result;
}
private void walkSchema(ExecutionContext executionContext, Map.Entry<String, JsonSchema> entry, JsonNode node,
JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema,
- SetView<ValidationMessage> validationMessages, WalkListenerRunner propertyWalkListenerRunner) {
+ SetView<ValidationMessage> validationMessages, WalkListenerRunner propertyWalkListenerRunner, boolean skipWalk) {
JsonSchema propertySchema = entry.getValue();
JsonNode propertyNode = (node == null ? null : node.get(entry.getKey()));
JsonNodePath path = instanceLocation.append(entry.getKey());
boolean executeWalk = propertyWalkListenerRunner.runPreWalkListeners(executionContext,
ValidatorTypeCode.PROPERTIES.getValue(), propertyNode, rootNode, path,
- propertySchema.getEvaluationPath(), propertySchema.getSchemaLocation(), propertySchema.getSchemaNode(),
- propertySchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory());
- if (executeWalk) {
+ propertySchema, this);
+ if (propertyNode == null && node != null) {
+ // Attempt to get the property node again in case the propertyNode was updated
+ propertyNode = node.get(entry.getKey());
+ }
+ if (executeWalk && !(skipWalk && propertyNode == null)) {
validationMessages.union(
propertySchema.walk(executionContext, propertyNode, rootNode, path, shouldValidateSchema));
}
propertyWalkListenerRunner.runPostWalkListeners(executionContext, ValidatorTypeCode.PROPERTIES.getValue(), propertyNode,
- rootNode, path, propertySchema.getEvaluationPath(),
- propertySchema.getSchemaLocation(), propertySchema.getSchemaNode(), propertySchema.getParentSchema(), this.validationContext, this.validationContext.getJsonSchemaFactory(), validationMessages);
-
+ rootNode, path, propertySchema,
+ this, validationMessages);
}
public Map<String, JsonSchema> getSchemas() {
diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 0ca0f26..e73bb7d 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -28,7 +28,7 @@ import java.util.*; public class RecursiveRefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class);
- protected JsonSchemaRef schema;
+ protected final JsonSchemaRef schema;
public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext);
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index 1959231..1a32e6e 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -28,7 +28,7 @@ import java.util.*; public class RefValidator extends BaseJsonValidator {
private static final Logger logger = LoggerFactory.getLogger(RefValidator.class);
- protected JsonSchemaRef schema;
+ protected final JsonSchemaRef schema;
private static final String REF_CURRENT = "#";
diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index 7f7736e..245f690 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -29,16 +29,16 @@ import java.util.*; public class TypeValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(TypeValidator.class); - private JsonType schemaType; - private JsonSchema parentSchema; - private UnionTypeValidator unionTypeValidator; + private final JsonType schemaType; + private final UnionTypeValidator unionTypeValidator; public TypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.TYPE, validationContext); this.schemaType = TypeFactory.getSchemaNodeType(schemaNode); - this.parentSchema = parentSchema; if (this.schemaType == JsonType.UNION) { this.unionTypeValidator = new UnionTypeValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext); + } else { + this.unionTypeValidator = null; } } @@ -47,7 +47,7 @@ public class TypeValidator extends BaseJsonValidator { } public boolean equalsToSchemaType(JsonNode node) { - return JsonNodeUtil.equalsToSchemaType(node,this.schemaType, this.parentSchema, this.validationContext); + return JsonNodeUtil.equalsToSchemaType(node, this.schemaType, this.parentSchema, this.validationContext); } @Override diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index be43b96..a061b97 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -30,12 +30,14 @@ import java.util.Set; public class UniqueItemsValidator extends BaseJsonValidator implements JsonValidator {
private static final Logger logger = LoggerFactory.getLogger(UniqueItemsValidator.class);
- private boolean unique = false;
+ private final boolean unique;
public UniqueItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNIQUE_ITEMS, validationContext);
if (schemaNode.isBoolean()) {
unique = schemaNode.booleanValue();
+ } else {
+ unique = false;
}
}
diff --git a/src/main/java/com/networknt/schema/utils/JsonNodes.java b/src/main/java/com/networknt/schema/utils/JsonNodes.java new file mode 100644 index 0000000..5653959 --- /dev/null +++ b/src/main/java/com/networknt/schema/utils/JsonNodes.java @@ -0,0 +1,82 @@ +/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.networknt.schema.utils;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.networknt.schema.JsonNodePath;
+
+/**
+ * Utility methods for JsonNode.
+ */
+public class JsonNodes {
+ /**
+ * Gets the node found at the path.
+ *
+ * @param node the node
+ * @param path the path
+ * @return the node found at the path or null
+ */
+ @SuppressWarnings("unchecked")
+ public static <T extends JsonNode> T get(JsonNode node, JsonNodePath path) {
+ int nameCount = path.getNameCount();
+ JsonNode current = node;
+ for (int x = 0; x < nameCount; x++) {
+ Object segment = path.getElement(x);
+ JsonNode result = get(current, segment);
+ if (result == null) {
+ return null;
+ }
+ current = result;
+ }
+ return (T) current;
+ }
+
+ /**
+ * Gets the node given the property or index.
+ *
+ * @param node the node
+ * @param propertyOrIndex the property or index
+ * @return the node given the property or index
+ */
+ @SuppressWarnings("unchecked")
+ public static <T extends JsonNode> T get(JsonNode node, Object propertyOrIndex) {
+ JsonNode value = null;
+ if (propertyOrIndex instanceof Number) {
+ value = node.get(((Number) propertyOrIndex).intValue());
+ } else {
+ // In the case of string this represents an escaped json pointer and thus does not reflect the property directly
+ String unescaped = propertyOrIndex.toString();
+ if (unescaped.contains("~")) {
+ unescaped = unescaped.replace("~1", "/");
+ unescaped = unescaped.replace("~0", "~");
+ }
+ if (unescaped.contains("%")) {
+ try {
+ unescaped = URLDecoder.decode(unescaped, StandardCharsets.UTF_8.toString());
+ } catch (UnsupportedEncodingException e) {
+ // Do nothing
+ }
+ }
+
+ value = node.get(unescaped);
+ }
+ return (T) value;
+ }
+}
diff --git a/src/main/java/com/networknt/schema/utils/JsonSchemaRefs.java b/src/main/java/com/networknt/schema/utils/JsonSchemaRefs.java new file mode 100644 index 0000000..9a6231f --- /dev/null +++ b/src/main/java/com/networknt/schema/utils/JsonSchemaRefs.java @@ -0,0 +1,49 @@ +/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.networknt.schema.utils;
+
+import com.networknt.schema.DynamicRefValidator;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaRef;
+import com.networknt.schema.JsonValidator;
+import com.networknt.schema.RecursiveRefValidator;
+import com.networknt.schema.RefValidator;
+
+/**
+ * Utility methods for JsonSchemaRef.
+ */
+public class JsonSchemaRefs {
+
+ /**
+ * Gets the ref.
+ *
+ * @param schema the schema
+ * @return the ref
+ */
+ public static JsonSchemaRef from(JsonSchema schema) {
+ for (JsonValidator validator : schema.getValidators()) {
+ if (validator instanceof RefValidator) {
+ return ((RefValidator) validator).getSchemaRef();
+ } else if (validator instanceof DynamicRefValidator) {
+ return ((DynamicRefValidator) validator).getSchemaRef();
+ } else if (validator instanceof RecursiveRefValidator) {
+ return ((RecursiveRefValidator) validator).getSchemaRef();
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java index 57ea0f2..92d564c 100644 --- a/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/AbstractWalkListenerRunner.java @@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.ValidationContext; +import com.networknt.schema.JsonValidator; import com.networknt.schema.ValidationMessage; import java.util.List; @@ -14,14 +12,11 @@ import java.util.Set; public abstract class AbstractWalkListenerRunner implements WalkListenerRunner { - protected WalkEvent constructWalkEvent(ExecutionContext executionContext, String keyword, JsonNode node, - JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory) { + protected WalkEvent constructWalkEvent(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + JsonNode rootNode, JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator) { return WalkEvent.builder().executionContext(executionContext).instanceLocation(instanceLocation) - .evaluationPath(evaluationPath).keyword(keyword).node(node).parentSchema(parentSchema) - .rootNode(rootNode).schemaNode(schemaNode).schemaLocation(schemaLocation) - .currentJsonSchemaFactory(currentJsonSchemaFactory).validationContext(validationContext).build(); + .keyword(keyword).instanceNode(instanceNode) + .rootNode(rootNode).schema(schema).validator(validator).build(); } protected boolean runPreWalkListeners(List<JsonSchemaWalkListener> walkListeners, WalkEvent walkEvent) { diff --git a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java index 429771c..eba7f05 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultItemWalkListenerRunner.java @@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.ValidationContext; +import com.networknt.schema.JsonValidator; import com.networknt.schema.ValidationMessage; import java.util.List; @@ -21,22 +19,18 @@ public class DefaultItemWalkListenerRunner extends AbstractWalkListenerRunner { } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, - JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, - evaluationPath, schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + JsonNode rootNode, JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, + schema, validator); return runPreWalkListeners(itemWalkListeners, walkEvent); } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, - JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory currentJsonSchemaFactory, Set<ValidationMessage> validationMessages) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, - evaluationPath, schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + JsonNode rootNode, JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator, Set<ValidationMessage> validationMessages) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, + schema, validator); runPostWalkListeners(itemWalkListeners, walkEvent, validationMessages); } diff --git a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java index 04b1106..0435f53 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultKeywordWalkListenerRunner.java @@ -16,13 +16,10 @@ public class DefaultKeywordWalkListenerRunner extends AbstractWalkListenerRunner } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, - JsonSchema parentSchema, ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, + JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator) { boolean continueRunningListenersAndWalk = true; - WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, - schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); // Run Listeners that are setup only for this keyword. List<JsonSchemaWalkListener> currentKeywordListeners = keywordWalkListenersMap.get(keyword); continueRunningListenersAndWalk = runPreWalkListeners(currentKeywordListeners, keywordWalkEvent); @@ -36,13 +33,9 @@ public class DefaultKeywordWalkListenerRunner extends AbstractWalkListenerRunner } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, - JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, - JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory, Set<ValidationMessage> validationMessages) { - WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, - schemaLocation, schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, JsonNodePath instanceLocation, + JsonSchema schema, JsonValidator validator, Set<ValidationMessage> validationMessages) { + WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); // Run Listeners that are setup only for this keyword. List<JsonSchemaWalkListener> currentKeywordListeners = keywordWalkListenersMap.get(keyword); runPostWalkListeners(currentKeywordListeners, keywordWalkEvent, validationMessages); diff --git a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java index a8e937d..e10253f 100644 --- a/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/DefaultPropertyWalkListenerRunner.java @@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.ValidationContext; +import com.networknt.schema.JsonValidator; import com.networknt.schema.ValidationMessage; import java.util.List; @@ -21,20 +19,16 @@ public class DefaultPropertyWalkListenerRunner extends AbstractWalkListenerRunne } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNode schemaNode, - JsonSchema parentSchema, ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, schemaLocation, - schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, + JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); return runPreWalkListeners(propertyWalkListeners, walkEvent); } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, - JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNode schemaNode, JsonSchema parentSchema, - ValidationContext validationContext, JsonSchemaFactory currentJsonSchemaFactory, Set<ValidationMessage> validationMessages) { - WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, node, rootNode, instanceLocation, evaluationPath, schemaLocation, - schemaNode, parentSchema, validationContext, currentJsonSchemaFactory); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, JsonNodePath instanceLocation, + JsonSchema schema, JsonValidator validator, Set<ValidationMessage> validationMessages) { + WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); runPostWalkListeners(propertyWalkListeners, walkEvent, validationMessages); } diff --git a/src/main/java/com/networknt/schema/walk/WalkEvent.java b/src/main/java/com/networknt/schema/walk/WalkEvent.java index bf7cf6c..f0a2079 100644 --- a/src/main/java/com/networknt/schema/walk/WalkEvent.java +++ b/src/main/java/com/networknt/schema/walk/WalkEvent.java @@ -4,10 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.SchemaValidatorsConfig; -import com.networknt.schema.ValidationContext; +import com.networknt.schema.JsonValidator; /** * Encapsulation of Walk data that is passed into the {@link JsonSchemaWalkListener}. @@ -15,73 +12,89 @@ import com.networknt.schema.ValidationContext; public class WalkEvent { private ExecutionContext executionContext; - private SchemaLocation schemaLocation; - private JsonNodePath evaluationPath; - private JsonNode schemaNode; - private JsonSchema parentSchema; + private JsonSchema schema; private String keyword; - private JsonNode node; private JsonNode rootNode; + private JsonNode instanceNode; private JsonNodePath instanceLocation; - private JsonSchemaFactory currentJsonSchemaFactory; - private ValidationContext validationContext; - + private JsonValidator validator; + + /** + * Gets the execution context. + * <p> + * As the listeners should be state-less, this allows listeners to store data in + * the collector context. + * + * @return the execution context + */ public ExecutionContext getExecutionContext() { return executionContext; } - public SchemaLocation getSchemaLocation() { - return schemaLocation; - } - - public JsonNodePath getEvaluationPath() { - return evaluationPath; - } - - public JsonNode getSchemaNode() { - return schemaNode; - } - - public JsonSchema getParentSchema() { - return parentSchema; + /** + * Gets the schema that will be used to evaluate the instance node. + * <p> + * For the keyword listener, this will allow getting the validator for the given keyword. + * + * @return the schema + */ + public JsonSchema getSchema() { + return schema; } + /** + * Gets the keyword. + * + * @return the keyword + */ public String getKeyword() { return keyword; } - public JsonNode getNode() { - return node; - } - + /** + * Gets the root instance node. + * <p> + * This makes it possible to get the parent node, for instance by getting the + * instance location parent and using the root node. + * + * @return the root node + */ public JsonNode getRootNode() { return rootNode; } - public JsonNodePath getInstanceLocation() { - return instanceLocation; + /** + * Gets the instance node. + * + * @return the instance node + */ + public JsonNode getInstanceNode() { + return instanceNode; } - public JsonSchema getRefSchema(SchemaLocation schemaUri) { - return currentJsonSchemaFactory.getSchema(schemaUri, validationContext.getConfig()); - } - - public JsonSchema getRefSchema(SchemaLocation schemaUri, SchemaValidatorsConfig schemaValidatorsConfig) { - if (schemaValidatorsConfig != null) { - return currentJsonSchemaFactory.getSchema(schemaUri, schemaValidatorsConfig); - } else { - return getRefSchema(schemaUri); - } + /** + * Gets the instance location of the instance node. + * + * @return the instance location of the instance node + */ + public JsonNodePath getInstanceLocation() { + return instanceLocation; } - public JsonSchemaFactory getCurrentJsonSchemaFactory() { - return currentJsonSchemaFactory; + /** + * Gets the validator that corresponds with the keyword. + * + * @return the validator + */ + @SuppressWarnings("unchecked") + public <T extends JsonValidator> T getValidator() { + return (T) this.validator; } @Override public String toString() { - return "WalkEvent [evaluationPath=" + evaluationPath + ", schemaLocation=" + schemaLocation - + ", instanceLocation=" + instanceLocation + "]"; + return "WalkEvent [evaluationPath=" + getSchema().getEvaluationPath() + ", schemaLocation=" + + getSchema().getSchemaLocation() + ", instanceLocation=" + instanceLocation + "]"; } static class WalkEventBuilder { @@ -97,23 +110,8 @@ public class WalkEvent { return this; } - public WalkEventBuilder evaluationPath(JsonNodePath evaluationPath) { - walkEvent.evaluationPath = evaluationPath; - return this; - } - - public WalkEventBuilder schemaLocation(SchemaLocation schemaLocation) { - walkEvent.schemaLocation = schemaLocation; - return this; - } - - public WalkEventBuilder schemaNode(JsonNode schemaNode) { - walkEvent.schemaNode = schemaNode; - return this; - } - - public WalkEventBuilder parentSchema(JsonSchema parentSchema) { - walkEvent.parentSchema = parentSchema; + public WalkEventBuilder schema(JsonSchema schema) { + walkEvent.schema = schema; return this; } @@ -122,8 +120,8 @@ public class WalkEvent { return this; } - public WalkEventBuilder node(JsonNode node) { - walkEvent.node = node; + public WalkEventBuilder instanceNode(JsonNode node) { + walkEvent.instanceNode = node; return this; } @@ -137,13 +135,8 @@ public class WalkEvent { return this; } - public WalkEventBuilder currentJsonSchemaFactory(JsonSchemaFactory currentJsonSchemaFactory) { - walkEvent.currentJsonSchemaFactory = currentJsonSchemaFactory; - return this; - } - - public WalkEventBuilder validationContext(ValidationContext validationContext) { - walkEvent.validationContext = validationContext; + public WalkEventBuilder validator(JsonValidator validator) { + walkEvent.validator = validator; return this; } @@ -156,5 +149,4 @@ public class WalkEvent { public static WalkEventBuilder builder() { return new WalkEventBuilder(); } - } diff --git a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java index 4bba2c6..0476d8b 100644 --- a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java @@ -4,23 +4,17 @@ import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.JsonNodePath; import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SchemaLocation; -import com.networknt.schema.ValidationContext; +import com.networknt.schema.JsonValidator; import com.networknt.schema.ValidationMessage; import java.util.Set; public interface WalkListenerRunner { - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, - JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory jsonSchemaFactory); + public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + JsonNode rootNode, JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator); - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode node, - JsonNode rootNode, JsonNodePath instanceLocation, JsonNodePath evaluationPath, SchemaLocation schemaLocation, - JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, - JsonSchemaFactory jsonSchemaFactory, Set<ValidationMessage> validationMessages); + public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + JsonNode rootNode, JsonNodePath instanceLocation, JsonSchema schema, JsonValidator validator, Set<ValidationMessage> validationMessages); } diff --git a/src/test/java/com/networknt/schema/Issue451Test.java b/src/test/java/com/networknt/schema/Issue451Test.java index e01e430..53a829e 100644 --- a/src/test/java/com/networknt/schema/Issue451Test.java +++ b/src/test/java/com/networknt/schema/Issue451Test.java @@ -70,7 +70,7 @@ public class Issue451Test { private static class CountingWalker implements JsonSchemaWalkListener { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - SchemaLocation path = walkEvent.getSchemaLocation(); + SchemaLocation path = walkEvent.getSchema().getSchemaLocation(); collector(walkEvent.getExecutionContext()).compute(path.toString(), (k, v) -> v == null ? 1 : v + 1); return WalkFlow.CONTINUE; } diff --git a/src/test/java/com/networknt/schema/Issue467Test.java b/src/test/java/com/networknt/schema/Issue467Test.java index 45a52db..ad8bb14 100644 --- a/src/test/java/com/networknt/schema/Issue467Test.java +++ b/src/test/java/com/networknt/schema/Issue467Test.java @@ -48,7 +48,7 @@ public class Issue467Test { config.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new JsonSchemaWalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - properties.add(walkEvent.getEvaluationPath()); + properties.add(walkEvent.getSchema().getEvaluationPath().append(walkEvent.getKeyword())); return WalkFlow.CONTINUE; } @@ -72,7 +72,7 @@ public class Issue467Test { config.addPropertyWalkListener(new JsonSchemaWalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - properties.add(walkEvent.getEvaluationPath()); + properties.add(walkEvent.getSchema().getEvaluationPath()); return WalkFlow.CONTINUE; } @@ -84,7 +84,7 @@ public class Issue467Test { JsonNode data = mapper.readTree(Issue467Test.class.getResource("/data/issue467.json")); ValidationResult result = schema.walk(data, true); assertEquals( - new HashSet<>(Arrays.asList("$.properties.tags", "$.properties.tags.items[0].properties.category")), + new HashSet<>(Arrays.asList("$.properties.tags", "$.properties.tags.items[0].properties.category", "$.properties.tags.items[0].properties.value")), properties.stream().map(Object::toString).collect(Collectors.toSet())); assertEquals(1, result.getValidationMessages().size()); } diff --git a/src/test/java/com/networknt/schema/Issue724Test.java b/src/test/java/com/networknt/schema/Issue724Test.java index 03f0e3a..47b060c 100644 --- a/src/test/java/com/networknt/schema/Issue724Test.java +++ b/src/test/java/com/networknt/schema/Issue724Test.java @@ -65,14 +65,14 @@ class Issue724Test { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { boolean isString = - Optional.of(walkEvent.getSchemaNode()) + Optional.of(walkEvent.getSchema().getSchemaNode()) .map(jsonNode -> jsonNode.get("type")) .map(JsonNode::asText) .map(type -> type.equals("string")) .orElse(false); if (isString) { - this.strings.add(walkEvent.getNode().asText()); + this.strings.add(walkEvent.getInstanceNode().asText()); } return WalkFlow.CONTINUE; diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java index 492bb5a..3ff08e1 100644 --- a/src/test/java/com/networknt/schema/JsonWalkTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkTest.java @@ -162,15 +162,15 @@ public class JsonWalkTest { public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { ObjectMapper mapper = new ObjectMapper(); String keyWordName = keywordWalkEvent.getKeyword(); - JsonNode schemaNode = keywordWalkEvent.getSchemaNode(); + JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); CollectorContext collectorContext = keywordWalkEvent.getExecutionContext().getCollectorContext(); if (collectorContext.get(SAMPLE_WALK_COLLECTOR_TYPE) == null) { collectorContext.add(SAMPLE_WALK_COLLECTOR_TYPE, mapper.createObjectNode()); } if (keyWordName.equals(CUSTOM_KEYWORD) && schemaNode.get(CUSTOM_KEYWORD).isArray()) { ObjectNode objectNode = (ObjectNode) collectorContext.get(SAMPLE_WALK_COLLECTOR_TYPE); - objectNode.put(keywordWalkEvent.getSchemaNode().get("title").textValue().toUpperCase(), - keywordWalkEvent.getNode().textValue()); + objectNode.put(keywordWalkEvent.getSchema().getSchemaNode().get("title").textValue().toUpperCase(), + keywordWalkEvent.getInstanceNode().textValue()); } return WalkFlow.CONTINUE; } @@ -191,8 +191,8 @@ public class JsonWalkTest { collectorContext.add(SAMPLE_WALK_COLLECTOR_TYPE, mapper.createObjectNode()); } ObjectNode objectNode = (ObjectNode) collectorContext.get(SAMPLE_WALK_COLLECTOR_TYPE); - objectNode.set(keywordWalkEvent.getSchemaNode().get("title").textValue().toLowerCase(), - keywordWalkEvent.getNode()); + objectNode.set(keywordWalkEvent.getSchema().getSchemaNode().get("title").textValue().toLowerCase(), + keywordWalkEvent.getInstanceNode()); return WalkFlow.SKIP; } @@ -206,7 +206,7 @@ public class JsonWalkTest { @Override public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { - JsonNode schemaNode = keywordWalkEvent.getSchemaNode(); + JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); if (schemaNode.get("title").textValue().equals("Property3")) { return WalkFlow.SKIP; } diff --git a/src/test/java/com/networknt/schema/walk/JsonSchemaWalkListenerTest.java b/src/test/java/com/networknt/schema/walk/JsonSchemaWalkListenerTest.java new file mode 100644 index 0000000..d4b6701 --- /dev/null +++ b/src/test/java/com/networknt/schema/walk/JsonSchemaWalkListenerTest.java @@ -0,0 +1,814 @@ +/*
+ * Copyright (c) 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.networknt.schema.walk;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.ApplyDefaultsStrategy;
+import com.networknt.schema.InputFormat;
+import com.networknt.schema.ItemsValidator;
+import com.networknt.schema.ItemsValidator202012;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.JsonSchemaRef;
+import com.networknt.schema.PathType;
+import com.networknt.schema.PropertiesValidator;
+import com.networknt.schema.SchemaId;
+import com.networknt.schema.SchemaLocation;
+import com.networknt.schema.SchemaValidatorsConfig;
+import com.networknt.schema.SpecVersion.VersionFlag;
+import com.networknt.schema.serialization.JsonMapperFactory;
+import com.networknt.schema.utils.JsonNodes;
+import com.networknt.schema.utils.JsonSchemaRefs;
+import com.networknt.schema.ValidationMessage;
+import com.networknt.schema.ValidationResult;
+import com.networknt.schema.ValidatorTypeCode;
+
+/**
+ * JsonSchemaWalkListenerTest.
+ */
+class JsonSchemaWalkListenerTest {
+
+ @Test
+ void keywordListener() {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"description\": \"Default Description\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"tags\": {\r\n"
+ + " \"type\": \"array\",\r\n"
+ + " \"items\": {\r\n"
+ + " \"$ref\": \"#/definitions/tag\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"definitions\": {\r\n"
+ + " \"tag\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"name\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"description\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setPathType(PathType.JSON_POINTER);
+ config.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> propertyKeywords = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext()
+ .getCollectorMap().computeIfAbsent("propertyKeywords", key -> new ArrayList<>());
+ propertyKeywords.add(walkEvent);
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schemaData, config);
+ String inputData = "{\r\n"
+ + " \"tags\": [\r\n"
+ + " {\r\n"
+ + " \"name\": \"image\",\r\n"
+ + " \"description\": \"An image\"\r\n"
+ + " },\r\n"
+ + " {\r\n"
+ + " \"name\": \"link\",\r\n"
+ + " \"description\": \"A link\"\r\n"
+ + " }\r\n"
+ + " ]\r\n"
+ + "}";
+ ValidationResult result = schema.walk(inputData, InputFormat.JSON, true);
+ assertTrue(result.getValidationMessages().isEmpty());
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> propertyKeywords = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("propertyKeywords");
+ assertEquals(3, propertyKeywords.size());
+ assertEquals("properties", propertyKeywords.get(0).getValidator().getKeyword());
+ assertEquals("", propertyKeywords.get(0).getInstanceLocation().toString());
+ assertEquals("/properties", propertyKeywords.get(0).getSchema().getEvaluationPath()
+ .append(propertyKeywords.get(0).getKeyword()).toString());
+ assertEquals("/tags/0", propertyKeywords.get(1).getInstanceLocation().toString());
+ assertEquals("image", propertyKeywords.get(1).getInstanceNode().get("name").asText());
+ assertEquals("/properties/tags/items/$ref/properties",
+ propertyKeywords.get(1).getValidator().getEvaluationPath().toString());
+ assertEquals("/properties/tags/items/$ref/properties", propertyKeywords.get(1).getSchema().getEvaluationPath()
+ .append(propertyKeywords.get(1).getKeyword()).toString());
+ assertEquals("/tags/1", propertyKeywords.get(2).getInstanceLocation().toString());
+ assertEquals("/properties/tags/items/$ref/properties", propertyKeywords.get(2).getSchema().getEvaluationPath()
+ .append(propertyKeywords.get(2).getKeyword()).toString());
+ assertEquals("link", propertyKeywords.get(2).getInstanceNode().get("name").asText());
+ }
+
+ @Test
+ void propertyListener() {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"description\": \"Default Description\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"tags\": {\r\n"
+ + " \"type\": \"array\",\r\n"
+ + " \"items\": {\r\n"
+ + " \"$ref\": \"#/definitions/tag\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"definitions\": {\r\n"
+ + " \"tag\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"name\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"description\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setPathType(PathType.JSON_POINTER);
+ config.addPropertyWalkListener(new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> properties = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext()
+ .getCollectorMap().computeIfAbsent("properties", key -> new ArrayList<>());
+ properties.add(walkEvent);
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schemaData, config);
+ String inputData = "{\r\n"
+ + " \"tags\": [\r\n"
+ + " {\r\n"
+ + " \"name\": \"image\",\r\n"
+ + " \"description\": \"An image\"\r\n"
+ + " },\r\n"
+ + " {\r\n"
+ + " \"name\": \"link\",\r\n"
+ + " \"description\": \"A link\"\r\n"
+ + " }\r\n"
+ + " ]\r\n"
+ + "}";
+ ValidationResult result = schema.walk(inputData, InputFormat.JSON, true);
+ assertTrue(result.getValidationMessages().isEmpty());
+
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> properties = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("properties");
+ assertEquals(5, properties.size());
+ assertEquals("properties", properties.get(0).getValidator().getKeyword());
+
+ assertEquals("/tags", properties.get(0).getInstanceLocation().toString());
+ assertEquals("/properties/tags", properties.get(0).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/0/name", properties.get(1).getInstanceLocation().toString());
+ assertEquals("image", properties.get(1).getInstanceNode().asText());
+ assertEquals("/properties/tags/items/$ref/properties/name", properties.get(1).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/0/description", properties.get(2).getInstanceLocation().toString());
+ assertEquals("An image", properties.get(2).getInstanceNode().asText());
+ assertEquals("/properties/tags/items/$ref/properties/description", properties.get(2).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/1/name", properties.get(3).getInstanceLocation().toString());
+ assertEquals("link", properties.get(3).getInstanceNode().asText());
+ assertEquals("/properties/tags/items/$ref/properties/name", properties.get(3).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/1/description", properties.get(4).getInstanceLocation().toString());
+ assertEquals("A link", properties.get(4).getInstanceNode().asText());
+ assertEquals("/properties/tags/items/$ref/properties/description", properties.get(4).getSchema().getEvaluationPath().toString());
+ }
+
+ @Test
+ void itemsListener() {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"description\": \"Default Description\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"tags\": {\r\n"
+ + " \"type\": \"array\",\r\n"
+ + " \"items\": {\r\n"
+ + " \"$ref\": \"#/definitions/tag\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"definitions\": {\r\n"
+ + " \"tag\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"name\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"description\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setPathType(PathType.JSON_POINTER);
+ config.addItemWalkListener(new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext()
+ .getCollectorMap().computeIfAbsent("items", key -> new ArrayList<>());
+ items.add(walkEvent);
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schemaData, config);
+ String inputData = "{\r\n"
+ + " \"tags\": [\r\n"
+ + " {\r\n"
+ + " \"name\": \"image\",\r\n"
+ + " \"description\": \"An image\"\r\n"
+ + " },\r\n"
+ + " {\r\n"
+ + " \"name\": \"link\",\r\n"
+ + " \"description\": \"A link\"\r\n"
+ + " }\r\n"
+ + " ]\r\n"
+ + "}";
+ ValidationResult result = schema.walk(inputData, InputFormat.JSON, true);
+ assertTrue(result.getValidationMessages().isEmpty());
+
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
+ assertEquals(2, items.size());
+ assertEquals("items", items.get(0).getValidator().getKeyword());
+ assertTrue(items.get(0).getValidator() instanceof ItemsValidator);
+
+ assertEquals("/tags/0", items.get(0).getInstanceLocation().toString());
+ assertEquals("/properties/tags/items", items.get(0).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/1", items.get(1).getInstanceLocation().toString());
+ assertEquals("/properties/tags/items", items.get(1).getSchema().getEvaluationPath().toString());
+ }
+
+ @Test
+ void items202012Listener() {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"description\": \"Default Description\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"tags\": {\r\n"
+ + " \"type\": \"array\",\r\n"
+ + " \"items\": {\r\n"
+ + " \"$ref\": \"#/definitions/tag\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"definitions\": {\r\n"
+ + " \"tag\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"name\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"description\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setPathType(PathType.JSON_POINTER);
+ config.addItemWalkListener(new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> items = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext()
+ .getCollectorMap().computeIfAbsent("items", key -> new ArrayList<>());
+ items.add(walkEvent);
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schemaData, config);
+ String inputData = "{\r\n"
+ + " \"tags\": [\r\n"
+ + " {\r\n"
+ + " \"name\": \"image\",\r\n"
+ + " \"description\": \"An image\"\r\n"
+ + " },\r\n"
+ + " {\r\n"
+ + " \"name\": \"link\",\r\n"
+ + " \"description\": \"A link\"\r\n"
+ + " }\r\n"
+ + " ]\r\n"
+ + "}";
+ ValidationResult result = schema.walk(inputData, InputFormat.JSON, true);
+ assertTrue(result.getValidationMessages().isEmpty());
+
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> items = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().get("items");
+ assertEquals(2, items.size());
+ assertEquals("items", items.get(0).getValidator().getKeyword());
+ assertTrue(items.get(0).getValidator() instanceof ItemsValidator202012);
+
+ assertEquals("/tags/0", items.get(0).getInstanceLocation().toString());
+ assertEquals("/properties/tags/items", items.get(0).getSchema().getEvaluationPath().toString());
+
+ assertEquals("/tags/1", items.get(1).getInstanceLocation().toString());
+ assertEquals("/properties/tags/items", items.get(1).getSchema().getEvaluationPath().toString());
+ }
+
+ @Test
+ void draft201909() {
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setPathType(PathType.JSON_POINTER);
+ config.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> propertyKeywords = (List<WalkEvent>) walkEvent.getExecutionContext().getCollectorContext()
+ .getCollectorMap().computeIfAbsent("propertyKeywords", key -> new ArrayList<>());
+ propertyKeywords.add(walkEvent);
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V201909)
+ .getSchema(SchemaLocation.of(SchemaId.V201909), config);
+ String inputData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2019-09/schema\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"kebab-case\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"snake_case\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"a\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+ ValidationResult result = schema.walk(inputData, InputFormat.JSON, true);
+ assertTrue(result.getValidationMessages().isEmpty());
+
+ @SuppressWarnings("unchecked")
+ List<WalkEvent> propertyKeywords = (List<WalkEvent>) result.getExecutionContext().getCollectorContext().getCollectorMap().get("propertyKeywords");
+
+ assertEquals(28, propertyKeywords.size());
+
+ assertEquals("", propertyKeywords.get(0).getInstanceLocation().toString());
+ assertEquals("/properties", propertyKeywords.get(0).getSchema().getEvaluationPath().append(propertyKeywords.get(0).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/schema#/properties", propertyKeywords.get(0).getSchema().getSchemaLocation().append(propertyKeywords.get(0).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(1).getInstanceLocation().toString());
+ assertEquals("/allOf/0/$ref/properties", propertyKeywords.get(1).getSchema().getEvaluationPath().append(propertyKeywords.get(1).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/core#/properties", propertyKeywords.get(1).getSchema().getSchemaLocation().append(propertyKeywords.get(1).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(2).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties", propertyKeywords.get(2).getSchema().getEvaluationPath().append(propertyKeywords.get(2).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/applicator#/properties", propertyKeywords.get(2).getSchema().getSchemaLocation().append(propertyKeywords.get(2).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(3).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/properties", propertyKeywords.get(3).getSchema().getEvaluationPath().append(propertyKeywords.get(3).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/schema#/properties", propertyKeywords.get(3).getSchema().getSchemaLocation().append(propertyKeywords.get(3).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(4).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/0/$ref/properties", propertyKeywords.get(4).getSchema().getEvaluationPath().append(propertyKeywords.get(4).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/core#/properties", propertyKeywords.get(4).getSchema().getSchemaLocation().append(propertyKeywords.get(4).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(5).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/1/$ref/properties", propertyKeywords.get(5).getSchema().getEvaluationPath().append(propertyKeywords.get(5).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/applicator#/properties", propertyKeywords.get(5).getSchema().getSchemaLocation().append(propertyKeywords.get(5).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(6).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/2/$ref/properties", propertyKeywords.get(6).getSchema().getEvaluationPath().append(propertyKeywords.get(6).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/validation#/properties", propertyKeywords.get(6).getSchema().getSchemaLocation().append(propertyKeywords.get(6).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(7).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/3/$ref/properties", propertyKeywords.get(7).getSchema().getEvaluationPath().append(propertyKeywords.get(7).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/meta-data#/properties", propertyKeywords.get(7).getSchema().getSchemaLocation().append(propertyKeywords.get(7).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(8).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/4/$ref/properties", propertyKeywords.get(8).getSchema().getEvaluationPath().append(propertyKeywords.get(8).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/format#/properties", propertyKeywords.get(8).getSchema().getSchemaLocation().append(propertyKeywords.get(8).getKeyword()).toString());
+
+ assertEquals("/properties/kebab-case", propertyKeywords.get(9).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/5/$ref/properties", propertyKeywords.get(9).getSchema().getEvaluationPath().append(propertyKeywords.get(9).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/content#/properties", propertyKeywords.get(9).getSchema().getSchemaLocation().append(propertyKeywords.get(9).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(10).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/properties", propertyKeywords.get(10).getSchema().getEvaluationPath().append(propertyKeywords.get(10).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/schema#/properties", propertyKeywords.get(10).getSchema().getSchemaLocation().append(propertyKeywords.get(10).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(11).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/0/$ref/properties", propertyKeywords.get(11).getSchema().getEvaluationPath().append(propertyKeywords.get(11).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/core#/properties", propertyKeywords.get(11).getSchema().getSchemaLocation().append(propertyKeywords.get(11).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(12).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/1/$ref/properties", propertyKeywords.get(12).getSchema().getEvaluationPath().append(propertyKeywords.get(12).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/applicator#/properties", propertyKeywords.get(12).getSchema().getSchemaLocation().append(propertyKeywords.get(12).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(13).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/2/$ref/properties", propertyKeywords.get(13).getSchema().getEvaluationPath().append(propertyKeywords.get(13).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/validation#/properties", propertyKeywords.get(13).getSchema().getSchemaLocation().append(propertyKeywords.get(13).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(14).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/3/$ref/properties", propertyKeywords.get(14).getSchema().getEvaluationPath().append(propertyKeywords.get(14).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/meta-data#/properties", propertyKeywords.get(14).getSchema().getSchemaLocation().append(propertyKeywords.get(14).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(15).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/4/$ref/properties", propertyKeywords.get(15).getSchema().getEvaluationPath().append(propertyKeywords.get(15).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/format#/properties", propertyKeywords.get(15).getSchema().getSchemaLocation().append(propertyKeywords.get(15).getKeyword()).toString());
+
+ assertEquals("/properties/snake_case", propertyKeywords.get(16).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/5/$ref/properties", propertyKeywords.get(16).getSchema().getEvaluationPath().append(propertyKeywords.get(16).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/content#/properties", propertyKeywords.get(16).getSchema().getSchemaLocation().append(propertyKeywords.get(16).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(17).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/properties", propertyKeywords.get(17).getSchema().getEvaluationPath().append(propertyKeywords.get(17).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/schema#/properties", propertyKeywords.get(17).getSchema().getSchemaLocation().append(propertyKeywords.get(17).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(18).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/0/$ref/properties", propertyKeywords.get(18).getSchema().getEvaluationPath().append(propertyKeywords.get(18).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/core#/properties", propertyKeywords.get(18).getSchema().getSchemaLocation().append(propertyKeywords.get(18).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(19).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/1/$ref/properties", propertyKeywords.get(19).getSchema().getEvaluationPath().append(propertyKeywords.get(19).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/applicator#/properties", propertyKeywords.get(19).getSchema().getSchemaLocation().append(propertyKeywords.get(19).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(20).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/2/$ref/properties", propertyKeywords.get(20).getSchema().getEvaluationPath().append(propertyKeywords.get(20).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/validation#/properties", propertyKeywords.get(20).getSchema().getSchemaLocation().append(propertyKeywords.get(20).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(21).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/3/$ref/properties", propertyKeywords.get(21).getSchema().getEvaluationPath().append(propertyKeywords.get(21).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/meta-data#/properties", propertyKeywords.get(21).getSchema().getSchemaLocation().append(propertyKeywords.get(21).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(22).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/4/$ref/properties", propertyKeywords.get(22).getSchema().getEvaluationPath().append(propertyKeywords.get(22).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/format#/properties", propertyKeywords.get(22).getSchema().getSchemaLocation().append(propertyKeywords.get(22).getKeyword()).toString());
+
+ assertEquals("/properties/a", propertyKeywords.get(23).getInstanceLocation().toString());
+ assertEquals("/allOf/1/$ref/properties/properties/additionalProperties/$recursiveRef/allOf/5/$ref/properties", propertyKeywords.get(23).getSchema().getEvaluationPath().append(propertyKeywords.get(23).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/content#/properties", propertyKeywords.get(23).getSchema().getSchemaLocation().append(propertyKeywords.get(23).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(24).getInstanceLocation().toString());
+ assertEquals("/allOf/2/$ref/properties", propertyKeywords.get(24).getSchema().getEvaluationPath().append(propertyKeywords.get(24).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/validation#/properties", propertyKeywords.get(24).getSchema().getSchemaLocation().append(propertyKeywords.get(24).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(25).getInstanceLocation().toString());
+ assertEquals("/allOf/3/$ref/properties", propertyKeywords.get(25).getSchema().getEvaluationPath().append(propertyKeywords.get(25).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/meta-data#/properties", propertyKeywords.get(25).getSchema().getSchemaLocation().append(propertyKeywords.get(25).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(26).getInstanceLocation().toString());
+ assertEquals("/allOf/4/$ref/properties", propertyKeywords.get(26).getSchema().getEvaluationPath().append(propertyKeywords.get(26).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/format#/properties", propertyKeywords.get(26).getSchema().getSchemaLocation().append(propertyKeywords.get(26).getKeyword()).toString());
+
+ assertEquals("", propertyKeywords.get(27).getInstanceLocation().toString());
+ assertEquals("/allOf/5/$ref/properties", propertyKeywords.get(27).getSchema().getEvaluationPath().append(propertyKeywords.get(27).getKeyword()).toString());
+ assertEquals("https://json-schema.org/draft/2019-09/meta/content#/properties", propertyKeywords.get(27).getSchema().getSchemaLocation().append(propertyKeywords.get(27).getKeyword()).toString());
+ }
+
+ @Test
+ void applyDefaults() throws JsonMappingException, JsonProcessingException {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
+ + " \"title\": \"\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"s\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": \"S\"\r\n"
+ + " },\r\n"
+ + " \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n"
+ + " },\r\n"
+ + " \"required\": [ \"s\", \"ref\" ],\r\n"
+ + "\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"r\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": \"REF\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.setApplyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true));
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
+ JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ ValidationResult result = schema.walk(inputNode, true);
+ assertEquals("{\"s\":\"S\",\"ref\":\"REF\"}", inputNode.toString());
+ assertTrue(result.getValidationMessages().isEmpty());
+ }
+
+ @Test
+ void applyDefaultsWithWalker() throws JsonMappingException, JsonProcessingException {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
+ + " \"title\": \"\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"s\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": \"S\"\r\n"
+ + " },\r\n"
+ + " \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n"
+ + " },\r\n"
+ + " \"required\": [ \"s\", \"ref\" ],\r\n"
+ + "\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"r\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": \"REF\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.addPropertyWalkListener(new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode()
+ || walkEvent.getInstanceNode().isNull()) {
+ JsonSchema schema = walkEvent.getSchema();
+ JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema);
+ if (schemaRef != null) {
+ schema = schemaRef.getSchema();
+ }
+ JsonNode defaultNode = schema.getSchemaNode().get("default");
+ if (defaultNode != null) {
+ ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(),
+ walkEvent.getInstanceLocation().getParent());
+ parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
+ }
+ }
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
+ JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ ValidationResult result = schema.walk(inputNode, true);
+ assertEquals("{\"s\":\"S\",\"ref\":\"REF\"}", inputNode.toString());
+ assertTrue(result.getValidationMessages().isEmpty());
+ }
+
+ @Test
+ void applyInvalidDefaultsWithWalker() throws JsonMappingException, JsonProcessingException {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
+ + " \"title\": \"\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"s\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": 1\r\n"
+ + " },\r\n"
+ + " \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n"
+ + " },\r\n"
+ + " \"required\": [ \"s\", \"ref\" ],\r\n"
+ + "\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"r\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"default\": \"REF\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.addPropertyWalkListener(new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode()
+ || walkEvent.getInstanceNode().isNull()) {
+ JsonSchema schema = walkEvent.getSchema();
+ JsonSchemaRef schemaRef = JsonSchemaRefs.from(schema);
+ if (schemaRef != null) {
+ schema = schemaRef.getSchema();
+ }
+ JsonNode defaultNode = schema.getSchemaNode().get("default");
+ if (defaultNode != null) {
+ ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(),
+ walkEvent.getInstanceLocation().getParent());
+ parentNode.set(walkEvent.getInstanceLocation().getName(-1), defaultNode);
+ }
+ }
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
+ JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ ValidationResult result = schema.walk(inputNode, true);
+ assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
+ assertFalse(result.getValidationMessages().isEmpty());
+
+ inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ result = schema.walk(inputNode, false);
+ assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString());
+ assertTrue(result.getValidationMessages().isEmpty());
+ }
+
+ @Test
+ void missingRequired() throws JsonMappingException, JsonProcessingException {
+ String schemaData = "{\r\n"
+ + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n"
+ + " \"title\": \"\",\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"s\": {\r\n"
+ + " \"type\": \"integer\"\r\n"
+ + " },\r\n"
+ + " \"ref\": { \"$ref\": \"#/$defs/r\" }\r\n"
+ + " },\r\n"
+ + " \"required\": [ \"s\", \"ref\" ],\r\n"
+ + "\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"r\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+ Map<String, JsonNode> missingSchemaNode = new LinkedHashMap<>();
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new JsonSchemaWalkListener() {
+
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ JsonNode requiredNode = walkEvent.getSchema().getSchemaNode().get("required");
+ List<String> requiredProperties = new ArrayList<>();
+ if (requiredNode != null) {
+ if (requiredNode.isArray()) {
+ for (JsonNode fieldName : requiredNode) {
+ requiredProperties.add(fieldName.asText());
+ }
+ }
+ }
+ for (String requiredProperty : requiredProperties) {
+ JsonNode propertyNode = walkEvent.getInstanceNode().get(requiredProperty);
+ if (propertyNode == null) {
+ // Get the schema
+ PropertiesValidator propertiesValidator = walkEvent.getValidator();
+ JsonSchema propertySchema = propertiesValidator.getSchemas().get(requiredProperty);
+ JsonSchemaRef schemaRef = JsonSchemaRefs.from(propertySchema);
+ if (schemaRef != null) {
+ propertySchema = schemaRef.getSchema();
+ }
+ missingSchemaNode.put(requiredProperty, propertySchema.getSchemaNode());
+ }
+ }
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
+ JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ ValidationResult result = schema.walk(inputNode, true);
+ assertFalse(result.getValidationMessages().isEmpty());
+ assertEquals("{\"type\":\"integer\"}", missingSchemaNode.get("s").toString());
+ assertEquals("{\"type\":\"string\"}", missingSchemaNode.get("ref").toString());
+ }
+
+ @Test
+ void generateDataWithWalker() throws JsonMappingException, JsonProcessingException {
+ Map<String, Supplier<String>> generators = new HashMap<>();
+ generators.put("name.findName", () -> "John Doe");
+ generators.put("internet.email", () -> "john.doe@gmail.com");
+
+ String schemaData = "{\r\n"
+ + " \"type\": \"object\",\r\n"
+ + " \"properties\": {\r\n"
+ + " \"name\": {\r\n"
+ + " \"$ref\": \"#/$defs/Name\"\r\n"
+ + " },\r\n"
+ + " \"email\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"faker\": \"internet.email\"\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"required\": [\r\n"
+ + " \"name\",\r\n"
+ + " \"email\"\r\n"
+ + " ],\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"Name\": {\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"faker\": \"name.findName\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+ config.addPropertyWalkListener(new JsonSchemaWalkListener() {
+ @Override
+ public WalkFlow onWalkStart(WalkEvent walkEvent) {
+ if (walkEvent.getInstanceNode() == null || walkEvent.getInstanceNode().isMissingNode()
+ || walkEvent.getInstanceNode().isNull()) {
+ JsonSchema schema = walkEvent.getSchema();
+ JsonSchemaRef schemaRef = null;
+ do {
+ schemaRef = JsonSchemaRefs.from(schema);
+ if (schemaRef != null) {
+ schema = schemaRef.getSchema();
+ }
+ } while (schemaRef != null);
+ JsonNode fakerNode = schema.getSchemaNode().get("faker");
+ if (fakerNode != null) {
+ String faker = fakerNode.asText();
+ String fakeData = generators.get(faker).get();
+ JsonNode fakeDataNode = JsonNodeFactory.instance.textNode(fakeData);
+ ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(),
+ walkEvent.getInstanceLocation().getParent());
+ parentNode.set(walkEvent.getInstanceLocation().getName(-1), fakeDataNode);
+ }
+ }
+ return WalkFlow.CONTINUE;
+ }
+
+ @Override
+ public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages) {
+ }
+ });
+
+ JsonSchema schema = JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schemaData, config);
+ JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}");
+ ValidationResult result = schema.walk(inputNode, true);
+ assertEquals("{\"name\":\"John Doe\",\"email\":\"john.doe@gmail.com\"}", inputNode.toString());
+ assertTrue(result.getValidationMessages().isEmpty());
+ }
+}
|