|
| 1 | +### JSON Schema Walkers |
| 2 | + |
| 3 | +There can be use-cases where we need the capability to walk through the given JsonNode allowing functionality beyond validation like collecting information,handling cross cutting concerns like logging or instrumentation. JSON walkers were introduced to complement the validation functionality this library already provides. |
| 4 | + |
| 5 | +Currently walking is defined at the validator instance level for all the built-in keywords. |
| 6 | + |
| 7 | +### Walk methods |
| 8 | + |
| 9 | +A new interface is introduced into the library that a Walker should implement. It should be noted that this interface also allows the validation based on shouldValidateSchema parameter. |
| 10 | + |
| 11 | +``` |
| 12 | +public interface JsonWalker { |
| 13 | + /** |
| 14 | + * |
| 15 | + * This method gives the capability to walk through the given JsonNode, allowing |
| 16 | + * functionality beyond validation like collecting information,handling cross |
| 17 | + * cutting concerns like logging or instrumentation. This method also performs |
| 18 | + * the validation if {@code shouldValidateSchema} is set to true. <br> |
| 19 | + * <br> |
| 20 | + * {@link BaseJsonValidator#walk(JsonNode, JsonNode, String, boolean)} provides |
| 21 | + * a default implementation of this method. However keywords that parse |
| 22 | + * sub-schemas should override this method to call walk method on those |
| 23 | + * subschemas. |
| 24 | + * |
| 25 | + * @param node JsonNode |
| 26 | + * @param rootNode JsonNode |
| 27 | + * @param at String |
| 28 | + * @param shouldValidateSchema boolean |
| 29 | + * @return a set of validation messages if shouldValidateSchema is true. |
| 30 | + */ |
| 31 | + Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema); |
| 32 | +} |
| 33 | +
|
| 34 | +``` |
| 35 | + |
| 36 | +The JSONValidator interface extends this new interface thus allowing all the validator's defined in library to implement this new interface. A default implementation of the walk method is provided in BaseJsonValidator class. In this case the walk method does nothing but validating based on shouldValidateSchema parameter. |
| 37 | + |
| 38 | +``` |
| 39 | +/** |
| 40 | + * This is default implementation of walk method. Its job is to call the |
| 41 | + * validate method if shouldValidateSchema is enabled. |
| 42 | + */ |
| 43 | + @Override |
| 44 | + public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { |
| 45 | + Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>(); |
| 46 | + if (shouldValidateSchema) { |
| 47 | + validationMessages = validate(node, rootNode, at); |
| 48 | + } |
| 49 | + return validationMessages; |
| 50 | + } |
| 51 | + |
| 52 | +``` |
| 53 | + |
| 54 | +A new walk method is introduced into JSONSchema class that allows us to walk through the JSONSchema. |
| 55 | + |
| 56 | +``` |
| 57 | + public ValidationResult walk(JsonNode node, boolean shouldValidateSchema) { |
| 58 | + // Create the collector context object. |
| 59 | + CollectorContext collectorContext = new CollectorContext(); |
| 60 | + // Set the collector context in thread info, this is unique for every thread. |
| 61 | + ThreadInfo.set(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, collectorContext); |
| 62 | + Set<ValidationMessage> errors = walk(node, node, AT_ROOT, shouldValidateSchema); |
| 63 | + // Load all the data from collectors into the context. |
| 64 | + collectorContext.loadCollectors(); |
| 65 | + // Collect errors and collector context into validation result. |
| 66 | + ValidationResult validationResult = new ValidationResult(errors, collectorContext); |
| 67 | + return validationResult; |
| 68 | + } |
| 69 | + |
| 70 | + @Override |
| 71 | + public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) { |
| 72 | + Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>(); |
| 73 | + // Walk through all the JSONWalker's. |
| 74 | + for (Entry<String, JsonValidator> entry : validators.entrySet()) { |
| 75 | + JsonWalker jsonWalker = entry.getValue(); |
| 76 | + String schemaPathWithKeyword = entry.getKey(); |
| 77 | + try { |
| 78 | + // Call all the pre-walk listeners. If all the pre-walk listeners return true |
| 79 | + // then continue to walk method. |
| 80 | + if (keywordWalkListenerRunner.runPreWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath, |
| 81 | + schemaNode, parentSchema)) { |
| 82 | + validationMessages.addAll(jsonWalker.walk(node, rootNode, at, shouldValidateSchema)); |
| 83 | + } |
| 84 | + } finally { |
| 85 | + // Call all the post-walk listeners. |
| 86 | + keywordWalkListenerRunner.runPostWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath, |
| 87 | + schemaNode, parentSchema, validationMessages); |
| 88 | + } |
| 89 | + } |
| 90 | + return validationMessages; |
| 91 | + } |
| 92 | +``` |
| 93 | +Following code snippet shows how to call the walk method on a JsonSchema instance. |
| 94 | + |
| 95 | +``` |
| 96 | +ValidationResult result = jsonSchema.walk(data,false); |
| 97 | +
|
| 98 | +``` |
| 99 | + |
| 100 | +walk method can be overridden for select validator's based on the use-case. Currently walk method has been overridden in PropertiesValidator,ItemsValidator,AllOfValidator,NotValidator,PatternValidator,RefValidator,AdditionalPropertiesValidator to accommodate the walk logic of the enclosed schema's. |
| 101 | + |
| 102 | +### Walk Listeners |
| 103 | + |
| 104 | +Walk listeners allows to execute a custom logic before and after a JsonWalker walk method is called. Walk listeners are modeled by a WalkListener interface. |
| 105 | + |
| 106 | +``` |
| 107 | +public interface WalkListener { |
| 108 | +
|
| 109 | + public boolean onWalkStart(WalkEvent walkEvent); |
| 110 | +
|
| 111 | + public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages); |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +Following is the example of a sample WalkListener implementation. |
| 116 | + |
| 117 | +``` |
| 118 | +private class PropertiesKeywordListener implements WalkListener { |
| 119 | +
|
| 120 | + @Override |
| 121 | + public boolean onWalkStart(WalkEvent keywordWalkEvent) { |
| 122 | + JsonNode schemaNode = keywordWalkEvent.getSchemaNode(); |
| 123 | + if(schemaNode.get("title").textValue().equals("Property3")) { |
| 124 | + return false; |
| 125 | + } |
| 126 | + return true; |
| 127 | + } |
| 128 | +
|
| 129 | + @Override |
| 130 | + public void onWalkEnd(WalkEvent keywordWalkEvent, Set<ValidationMessage> validationMessages) { |
| 131 | + |
| 132 | + } |
| 133 | + } |
| 134 | +``` |
| 135 | +If the onWalkStart method returns false, the actual walk method execution is skipped. |
| 136 | + |
| 137 | +Walk listeners can be added by using the JsonSchemaFactory class. |
| 138 | + |
| 139 | +``` |
| 140 | +final JsonSchemaFactory schemaFactory = JsonSchemaFactory |
| 141 | + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema) |
| 142 | + .addKeywordWalkListener(new AllKeywordListener()) |
| 143 | + .addKeywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener()) |
| 144 | + .addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), new PropertiesKeywordListener()).build(); |
| 145 | +this.jsonSchema = schemaFactory.getSchema(getSchema()); |
| 146 | + |
| 147 | +``` |
| 148 | + |
| 149 | +There are two kinds of walk listeners, keyword walk listeners and property walk listeners. Keyword walk listeners are called whenever the given keyword is encountered while walking the schema and JSON node data, for example we have added Ref and Property keyword walk listeners in the above example. Property walk listeners are called for every property defined in the JSON node data. |
| 150 | + |
| 151 | +Both property walk listeners and keyword walk listener are modeled by using the same WalkListener interface. Following is an example of how to add a property walk listener. |
| 152 | + |
| 153 | +``` |
| 154 | +final JsonSchemaFactory schemaFactory = JsonSchemaFactory |
| 155 | + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema) |
| 156 | + .addKeywordWalkListener(new AllKeywordListener()) |
| 157 | + .addPropertyWalkListener(new ExampleProperties()).build(); |
| 158 | +this.jsonSchema = schemaFactory.getSchema(getSchema()); |
| 159 | + |
| 160 | +``` |
| 161 | + |
| 162 | +### Walk Events |
| 163 | + |
| 164 | +An instance of WalkEvent is passed to both the onWalkStart and onWalkEnd methods of the WalkListeners implementations. |
| 165 | + |
| 166 | +A WalkEvent instance captures several details about the node currently being walked along with the schema of the node, Json path of the node and other details. |
| 167 | + |
| 168 | +Following snippet shows the details captured by WalkEvent instance. |
| 169 | + |
| 170 | +``` |
| 171 | +public class WalkEvent { |
| 172 | +
|
| 173 | + private String schemaPath; |
| 174 | + private JsonNode schemaNode; |
| 175 | + private JsonSchema parentSchema; |
| 176 | + private String keyWordName; |
| 177 | + private JsonNode node; |
| 178 | + private JsonNode rootNode; |
| 179 | + private String at; |
| 180 | +
|
| 181 | +``` |
0 commit comments