1### JSON Schema Walkers 2 3There 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, or applying default values. JSON walkers were introduced to complement the validation functionality this library already provides. 4 5Currently, walking is defined at the validator instance level for all the built-in keywords. 6 7### Walk methods 8 9A 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```java 12public interface JsonSchemaWalker { 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(ExecutionContext, JsonNode, JsonNode, JsonNodePath, boolean)} provides 21 * a default implementation of this method. However validators that parse 22 * sub-schemas should override this method to call walk method on those 23 * sub-schemas. 24 * 25 * @param executionContext ExecutionContext 26 * @param node JsonNode 27 * @param rootNode JsonNode 28 * @param instanceLocation JsonNodePath 29 * @param shouldValidateSchema boolean 30 * @return a set of validation messages if shouldValidateSchema is true. 31 */ 32 Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, 33 JsonNodePath instanceLocation, boolean shouldValidateSchema); 34} 35 36``` 37 38The 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. 39 40```java 41 /** 42 * This is default implementation of walk method. Its job is to call the 43 * validate method if shouldValidateSchema is enabled. 44 */ 45 @Override 46 default Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, 47 JsonNodePath instanceLocation, boolean shouldValidateSchema) { 48 return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) 49 : Collections.emptySet(); 50 } 51``` 52 53A new walk method added to the JSONSchema class allows us to walk through the JSONSchema. 54 55```java 56 public ValidationResult walk(JsonNode node, boolean validate) { 57 return walk(createExecutionContext(), node, validate); 58 } 59 60 @Override 61 public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, 62 JsonNodePath instanceLocation, boolean shouldValidateSchema) { 63 Set<ValidationMessage> errors = new LinkedHashSet<>(); 64 // Walk through all the JSONWalker's. 65 for (JsonValidator validator : getValidators()) { 66 JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath(); 67 try { 68 // Call all the pre-walk listeners. If at least one of the pre walk listeners 69 // returns SKIP, then skip the walk. 70 if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, 71 evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, 72 this, validator)) { 73 Set<ValidationMessage> results = null; 74 try { 75 results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); 76 } finally { 77 if (results != null && !results.isEmpty()) { 78 errors.addAll(results); 79 } 80 } 81 } 82 } finally { 83 // Call all the post-walk listeners. 84 this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, 85 evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, 86 this, validator, errors); 87 } 88 } 89 return errors; 90 } 91``` 92Following code snippet shows how to call the walk method on a JsonSchema instance. 93 94```java 95ValidationResult result = jsonSchema.walk(data, false); 96 97``` 98 99walk 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. 100 101### Walk Listeners 102 103Walk listeners allows to execute a custom logic before and after the invocation of a JsonWalker walk method. Walk listeners can be modeled by a WalkListener interface. 104 105```java 106public interface JsonSchemaWalkListener { 107 108 public WalkFlow onWalkStart(WalkEvent walkEvent); 109 110 public void onWalkEnd(WalkEvent walkEvent, Set<ValidationMessage> validationMessages); 111} 112``` 113 114Following is the example of a sample WalkListener implementation. 115 116```java 117private static class PropertiesKeywordListener implements JsonSchemaWalkListener { 118 119 @Override 120 public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { 121 JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); 122 if (schemaNode.get("title").textValue().equals("Property3")) { 123 return WalkFlow.SKIP; 124 } 125 return WalkFlow.CONTINUE; 126 } 127 128 @Override 129 public void onWalkEnd(WalkEvent keywordWalkEvent, Set<ValidationMessage> validationMessages) { 130 131 } 132 } 133``` 134If the onWalkStart method returns WalkFlow.SKIP, the actual walk method execution will be skipped. 135 136Walk listeners can be added by using the SchemaValidatorsConfig class. 137 138```java 139SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); 140 schemaValidatorsConfig.addKeywordWalkListener(new AllKeywordListener()); 141 schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener()); 142 schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), 143 new PropertiesKeywordListener()); 144final JsonSchemaFactory schemaFactory = JsonSchemaFactory 145 .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) 146 .build(); 147this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig); 148 149``` 150 151There are two kinds of walk listeners, keyword walk listeners and property walk listeners. Keyword walk listeners will be 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. 152 153Both property walk listeners and keyword walk listener can be modeled by using the same WalkListener interface. Following is an example of how to add a property walk listener. 154 155```java 156SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); 157schemaValidatorsConfig.addPropertyWalkListener(new ExamplePropertyWalkListener()); 158final JsonSchemaFactory schemaFactory = JsonSchemaFactory 159 .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) 160 .build(); 161this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig); 162 163``` 164 165### Walk Events 166 167An instance of WalkEvent is passed to both the onWalkStart and onWalkEnd methods of the WalkListeners implementations. 168 169A 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. 170 171Following snippet shows the details captured by WalkEvent instance. 172 173```java 174public class WalkEvent { 175 private ExecutionContext executionContext; 176 private JsonSchema schema; 177 private String keyword; 178 private JsonNode rootNode; 179 private JsonNode instanceNode; 180 private JsonNodePath instanceLocation; 181 private JsonValidator validator; 182 ... 183} 184``` 185 186### Sample Flow 187 188Given an example schema as shown, if we write a property listener, the walk flow is as depicted in the image. 189 190```json 191{ 192 193 "title": "Sample Schema", 194 "definitions" : { 195 "address" :{ 196 "street-address": { 197 "title": "Street Address", 198 "type": "string" 199 }, 200 "pincode": { 201 "title": "Body", 202 "type": "integer" 203 } 204 } 205 }, 206 "properties": { 207 "name": { 208 "title": "Title", 209 "type": "string", 210 "maxLength": 50 211 }, 212 "body": { 213 "title": "Body", 214 "type": "string" 215 }, 216 "address": { 217 "title": "Excerpt", 218 "$ref": "#/definitions/address" 219 } 220 221 }, 222 "additionalProperties": false 223} 224``` 225 226<!-- .element height="50%" width="50%" --> 227 228 229Few important points to note about the flow. 230 2311. onWalkStart and onWalkEnd are the methods defined in the property walk listener 2322. Anywhere during the flow, onWalkStart can return a WalkFlow.SKIP to stop the walk method execution of a particular "property schema". 2333. onWalkEnd will be called even if the onWalkStart returns a WalkFlow.SKIP. 2344. Walking a property will check if the keywords defined in the "property schema" has any keyword listeners, and they will be called in the defined order. 235 For example in the above schema when we walk through the "name" property if there are any keyword listeners defined for "type" or "maxlength" , they will be invoked in the defined order. 2365. Since we have a property listener defined, When we are walking through a property that has a "$ref" keyword which might have some more properties defined, 237 Our property listener would be invoked for each of the property defined in the "$ref" schema. 2386. As mentioned earlier anywhere during the "Walk Flow", we can return a WalkFlow.SKIP from onWalkStart method to stop the walk method of a particular "property schema" from being called. 239 Since the walk method will not be called any property or keyword listeners in the "property schema" will not be invoked. 240 241 242### Applying defaults 243 244In some use cases we may want to apply defaults while walking the schema. 245To accomplish this, create an ApplyDefaultsStrategy when creating a SchemaValidatorsConfig. 246The input object is changed in place, even if validation fails, or a fail-fast or some other exception is thrown. 247 248Here is the order of operations in walker. 2491. apply defaults 2501. run listeners 2511. validate if shouldValidateSchema is true 252 253Suppose the JSON schema is 254```json 255{ 256 "$schema": "http://json-schema.org/draft-04/schema#", 257 "title": "Schema with default values ", 258 "type": "object", 259 "properties": { 260 "intValue": { 261 "type": "integer", 262 "default": 15, 263 "minimum": 20 264 } 265 }, 266 "required": ["intValue"] 267} 268``` 269 270A JSON file like 271```json 272{ 273} 274``` 275 276would normally fail validation as "intValue" is required. 277But if we apply defaults while walking, then required validation passes, and the object is changed in place. 278 279```java 280 JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); 281 SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); 282 schemaValidatorsConfig.setApplyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)); 283 JsonSchema jsonSchema = schemaFactory.getSchema(SchemaLocation.of("classpath:schema.json"), schemaValidatorsConfig); 284 285 JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data.json")); 286 ValidationResult result = jsonSchema.walk(inputNode, true); 287 assertThat(result.getValidationMessages(), Matchers.empty()); 288 assertEquals("{\"intValue\":15}", inputNode.toString()); 289 assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()), 290 Matchers.containsInAnyOrder("$.intValue: must have a minimum value of 20.")); 291``` 292