Skip to content

Commit 57bae55

Browse files
authored
Merge pull request #336 from prashanthjos/master
Adding walk capabilities to networknt.
2 parents 03bf8ef + a3c2e97 commit 57bae55

31 files changed

+1290
-50
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ For the latest version, please check the [release](https://github.com/networknt/
114114

115115
## [Collector Context](doc/collector-context.md)
116116

117+
## [JSON Schema Walkers and WalkListeners](doc/walkers.md)
118+
117119
## [ECMA-262 Regex](doc/ecma-262.md)
118120

119121
## Known issues

doc/walk_flow.png

37.8 KB
Loading

doc/walkers.md

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
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+
```java
12+
public 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(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. BaseJsonValidator class provides a default implementation of the walk method. In this case the walk method does nothing but validating based on shouldValidateSchema parameter.
37+
38+
```java
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 added to the JSONSchema class allows us to walk through the JSONSchema.
55+
56+
```java
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 the invocation of a JsonWalker walk method. Walk listeners can be modeled by a WalkListener interface.
105+
106+
```java
107+
public interface JsonSchemaWalkListener {
108+
109+
public WalkFlow 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+
```java
118+
private static class PropertiesKeywordListener implements JsonSchemaWalkListener {
119+
120+
@Override
121+
public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) {
122+
JsonNode schemaNode = keywordWalkEvent.getSchemaNode();
123+
if (schemaNode.get("title").textValue().equals("Property3")) {
124+
return WalkFlow.SKIP;
125+
}
126+
return WalkFlow.CONTINUE;
127+
}
128+
129+
@Override
130+
public void onWalkEnd(WalkEvent keywordWalkEvent, Set<ValidationMessage> validationMessages) {
131+
132+
}
133+
}
134+
```
135+
If the onWalkStart method returns WalkFlow.SKIP, the actual walk method execution will be skipped.
136+
137+
Walk listeners can be added by using the SchemaValidatorsConfig class.
138+
139+
```java
140+
SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig();
141+
schemaValidatorsConfig.addKeywordWalkListener(new AllKeywordListener());
142+
schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener());
143+
schemaValidatorsConfig.addKeywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(),
144+
new PropertiesKeywordListener());
145+
final JsonSchemaFactory schemaFactory = JsonSchemaFactory
146+
.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema)
147+
.build();
148+
this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig);
149+
150+
```
151+
152+
There 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.
153+
154+
Both 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.
155+
156+
```java
157+
SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig();
158+
schemaValidatorsConfig.addPropertyWalkListener(new ExamplePropertyWalkListener());
159+
final JsonSchemaFactory schemaFactory = JsonSchemaFactory
160+
.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(metaSchema)
161+
.build();
162+
this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig);
163+
164+
```
165+
166+
### Walk Events
167+
168+
An instance of WalkEvent is passed to both the onWalkStart and onWalkEnd methods of the WalkListeners implementations.
169+
170+
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.
171+
172+
Following snippet shows the details captured by WalkEvent instance.
173+
174+
```java
175+
public class WalkEvent {
176+
177+
private String schemaPath;
178+
private JsonNode schemaNode;
179+
private JsonSchema parentSchema;
180+
private String keyWordName;
181+
private JsonNode node;
182+
private JsonNode rootNode;
183+
private String at;
184+
185+
```
186+
187+
### Sample Flow
188+
189+
Given an example schema as shown, if we write a property listener, the walk flow is as depicted in the image.
190+
191+
```json
192+
{
193+
194+
"title": "Sample Schema",
195+
"definitions" : {
196+
"address" :{
197+
"street-address": {
198+
"title": "Street Address",
199+
"type": "string"
200+
},
201+
"pincode": {
202+
"title": "Body",
203+
"type": "integer"
204+
}
205+
}
206+
},
207+
"properties": {
208+
"name": {
209+
"title": "Title",
210+
"type": "string",
211+
"maxLength": 50
212+
},
213+
"body": {
214+
"title": "Body",
215+
"type": "string"
216+
},
217+
"address": {
218+
"title": "Excerpt",
219+
"$ref": "#/definitions/address"
220+
}
221+
222+
},
223+
"additionalProperties": false
224+
}
225+
```
226+
227+
![img](walk_flow.png)<!-- .element height="50%" width="50%" -->
228+
229+
230+
Few important points to note about the flow.
231+
232+
1. onWalkStart and onWalkEnd are the methods defined in the property walk listener
233+
2. Anywhere during the flow, onWalkStart can return a WalkFlow.SKIP to stop the walk method execution of a particular "property schema".
234+
3. onWalkEnd will be called even if the onWalkStart returns a WalkFlow.SKIP.
235+
4. 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.
236+
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.
237+
5. 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,
238+
Our property listener would be invoked for each of the property defined in the "$ref" schema.
239+
6. 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.
240+
Since the walk method will not be called any property or keyword listeners in the "property schema" will not be invoked.

src/main/java/com/networknt/schema/AbstractJsonValidator.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616

1717
package com.networknt.schema;
1818

19-
import com.fasterxml.jackson.databind.JsonNode;
20-
2119
import java.util.Collections;
2220
import java.util.Map;
2321
import java.util.Set;
2422

23+
import com.fasterxml.jackson.databind.JsonNode;
24+
2525
public abstract class AbstractJsonValidator implements JsonValidator {
2626
private final String keyword;
2727

@@ -52,4 +52,13 @@ protected Set<ValidationMessage> fail(ErrorMessageType errorMessageType, String
5252
protected Set<ValidationMessage> fail(ErrorMessageType errorMessageType, String at, String... arguments) {
5353
return Collections.singleton(buildValidationMessage(errorMessageType, at, arguments));
5454
}
55+
56+
@Override
57+
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
58+
Set<ValidationMessage> validationMessages = Collections.emptySet();
59+
if (shouldValidateSchema) {
60+
validationMessages = validate(node, rootNode, at);
61+
}
62+
return validationMessages;
63+
}
5564
}

src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,42 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
101101
}
102102
return Collections.unmodifiableSet(errors);
103103
}
104+
105+
@Override
106+
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
107+
Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
108+
if (!node.isObject()) {
109+
return validationMessages;
110+
}
111+
// Validate schema if required.
112+
if (shouldValidateSchema) {
113+
validationMessages.addAll(validate(node, rootNode, at));
114+
}
115+
for (Iterator<String> it = node.fieldNames(); it.hasNext();) {
116+
String pname = it.next();
117+
// skip the context items
118+
if (pname.startsWith("#")) {
119+
continue;
120+
}
121+
boolean handledByPatternProperties = false;
122+
for (Pattern pattern : patternProperties) {
123+
Matcher m = pattern.matcher(pname);
124+
if (m.find()) {
125+
handledByPatternProperties = true;
126+
break;
127+
}
128+
}
129+
if (!allowedProperties.contains(pname) && !handledByPatternProperties) {
130+
if (allowAdditionalProperties) {
131+
if (additionalPropertiesSchema != null) {
132+
validationMessages.addAll(additionalPropertiesSchema.walk(node.get(pname), rootNode, at + "." + pname,
133+
shouldValidateSchema));
134+
}
135+
}
136+
}
137+
}
138+
139+
return validationMessages;
140+
}
104141

105142
}

src/main/java/com/networknt/schema/AllOfValidator.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
2626
private static final Logger logger = LoggerFactory.getLogger(AllOfValidator.class);
2727

2828
private List<JsonSchema> schemas = new ArrayList<JsonSchema>();
29-
29+
3030
public AllOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
3131
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext);
3232
int size = schemaNode.size();
@@ -47,5 +47,20 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
4747

4848
return Collections.unmodifiableSet(errors);
4949
}
50+
51+
@Override
52+
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
53+
Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
54+
55+
for (JsonSchema schema : schemas) {
56+
// Check if validation is needed.
57+
if (shouldValidateSchema) {
58+
validationMessages.addAll(schema.validate(node, rootNode, at));
59+
}
60+
// Walk through the schema
61+
validationMessages.addAll(schema.walk(node, rootNode, at, shouldValidateSchema));
62+
}
63+
return Collections.unmodifiableSet(validationMessages);
64+
}
5065

5166
}

0 commit comments

Comments
 (0)