xref: /aosp_15_r20/external/json-schema-validator/doc/walkers.md (revision 78c4dd6aa35290980cdcd1623a7e337e8d021c7c)
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![img](walk_flow.png)<!-- .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