Skip to content

Commit aeb48e2

Browse files
authored
Walk items schema instead of walking instance data (#993)
1 parent 3416e28 commit aeb48e2

14 files changed

+839
-63
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
165165

166166
@Override
167167
public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
168-
if (shouldValidateSchema) {
168+
if (shouldValidateSchema && node != null) {
169169
return validate(executionContext, node, rootNode, instanceLocation);
170170
}
171171

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode n
113113
.arguments(schemaNode.asText()).build();
114114
throw new InvalidSchemaRefException(validationMessage);
115115
}
116+
if (node == null) {
117+
// Check for circular dependency
118+
SchemaLocation schemaLocation = refSchema.getSchemaLocation();
119+
JsonSchema check = refSchema;
120+
boolean circularDependency = false;
121+
while (check.getEvaluationParentSchema() != null) {
122+
check = check.getEvaluationParentSchema();
123+
if (check.getSchemaLocation().equals(schemaLocation)) {
124+
circularDependency = true;
125+
break;
126+
}
127+
}
128+
if (circularDependency) {
129+
return Collections.emptySet();
130+
}
131+
}
116132
return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema);
117133
}
118134

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

Lines changed: 119 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -203,25 +203,125 @@ private boolean doValidate(ExecutionContext executionContext, SetView<Validation
203203

204204
@Override
205205
public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
206-
HashSet<ValidationMessage> validationMessages = new LinkedHashSet<>();
207-
if (node instanceof ArrayNode) {
208-
ArrayNode arrayNode = (ArrayNode) node;
209-
JsonNode defaultNode = null;
210-
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()
211-
&& this.schema != null) {
212-
defaultNode = getDefaultNode(this.schema);
206+
Set<ValidationMessage> validationMessages = new LinkedHashSet<>();
207+
boolean collectAnnotations = collectAnnotations();
208+
209+
// Add items annotation
210+
if (collectAnnotations || collectAnnotations(executionContext)) {
211+
if (this.schema != null) {
212+
// Applies to all
213+
executionContext.getAnnotations()
214+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
215+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
216+
.keyword(getKeyword()).value(true).build());
217+
} else if (this.tupleSchema != null) {
218+
// Tuples
219+
int items = node.isArray() ? node.size() : 1;
220+
int schemas = this.tupleSchema.size();
221+
if (items > schemas) {
222+
// More items than schemas so the keyword only applied to the number of schemas
223+
executionContext.getAnnotations()
224+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
225+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
226+
.keyword(getKeyword()).value(schemas).build());
227+
} else {
228+
// Applies to all
229+
executionContext.getAnnotations()
230+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
231+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
232+
.keyword(getKeyword()).value(true).build());
233+
}
213234
}
214-
int i = 0;
215-
for (JsonNode n : arrayNode) {
216-
if (n.isNull() && defaultNode != null) {
217-
arrayNode.set(i, defaultNode);
218-
n = defaultNode;
235+
}
236+
237+
if (this.schema != null) {
238+
// Walk the schema.
239+
if (node instanceof ArrayNode) {
240+
int count = Math.max(1, node.size());
241+
ArrayNode arrayNode = (ArrayNode) node;
242+
JsonNode defaultNode = null;
243+
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()) {
244+
defaultNode = getDefaultNode(this.schema);
245+
}
246+
for (int i = 0; i < count; i++) {
247+
JsonNode n = arrayNode.get(i);
248+
if (n != null) {
249+
if (n.isNull() && defaultNode != null) {
250+
arrayNode.set(i, defaultNode);
251+
n = defaultNode;
252+
}
253+
}
254+
walkSchema(executionContext, this.schema, n, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages, ValidatorTypeCode.ITEMS.getValue());
255+
}
256+
} else {
257+
walkSchema(executionContext, this.schema, null, rootNode, instanceLocation.append(0), shouldValidateSchema, validationMessages, ValidatorTypeCode.ITEMS.getValue());
258+
}
259+
}
260+
else if (this.tupleSchema != null) {
261+
int prefixItems = this.tupleSchema.size();
262+
for (int i = 0; i < prefixItems; i++) {
263+
// walk tuple schema
264+
if (node instanceof ArrayNode) {
265+
ArrayNode arrayNode = (ArrayNode) node;
266+
JsonNode defaultNode = null;
267+
JsonNode n = arrayNode.get(i);
268+
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()) {
269+
defaultNode = getDefaultNode(this.tupleSchema.get(i));
270+
}
271+
if (n != null) {
272+
if (n.isNull() && defaultNode != null) {
273+
arrayNode.set(i, defaultNode);
274+
n = defaultNode;
275+
}
276+
}
277+
walkSchema(executionContext, this.tupleSchema.get(i), n, rootNode, instanceLocation.append(i),
278+
shouldValidateSchema, validationMessages, ValidatorTypeCode.ITEMS.getValue());
279+
} else {
280+
walkSchema(executionContext, this.tupleSchema.get(i), null, rootNode, instanceLocation.append(i),
281+
shouldValidateSchema, validationMessages, ValidatorTypeCode.ITEMS.getValue());
282+
}
283+
}
284+
if (this.additionalSchema != null) {
285+
boolean hasAdditionalItem = false;
286+
287+
int additionalItems = Math.max(1, (node != null ? node.size() : 0) - prefixItems);
288+
for (int x = 0; x < additionalItems; x++) {
289+
int i = x + prefixItems;
290+
// walk additional item schema
291+
if (node instanceof ArrayNode) {
292+
ArrayNode arrayNode = (ArrayNode) node;
293+
JsonNode defaultNode = null;
294+
JsonNode n = arrayNode.get(i);
295+
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()) {
296+
defaultNode = getDefaultNode(this.additionalSchema);
297+
}
298+
if (n != null) {
299+
if (n.isNull() && defaultNode != null) {
300+
arrayNode.set(i, defaultNode);
301+
n = defaultNode;
302+
}
303+
}
304+
walkSchema(executionContext, this.additionalSchema, n, rootNode, instanceLocation.append(i),
305+
shouldValidateSchema, validationMessages, PROPERTY_ADDITIONAL_ITEMS);
306+
if (n != null) {
307+
hasAdditionalItem = true;
308+
}
309+
} else {
310+
walkSchema(executionContext, this.additionalSchema, null, rootNode, instanceLocation.append(i),
311+
shouldValidateSchema, validationMessages, PROPERTY_ADDITIONAL_ITEMS);
312+
}
313+
}
314+
315+
if (hasAdditionalItem) {
316+
if (collectAnnotations || collectAnnotations(executionContext, "additionalItems")) {
317+
executionContext.getAnnotations()
318+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
319+
.evaluationPath(this.additionalItemsEvaluationPath)
320+
.schemaLocation(this.additionalItemsSchemaLocation)
321+
.keyword("additionalItems").value(true).build());
322+
}
219323
}
220-
doWalk(executionContext, validationMessages, i, n, rootNode, instanceLocation, shouldValidateSchema);
221-
i++;
222324
}
223-
} else {
224-
doWalk(executionContext, validationMessages, 0, node, rootNode, instanceLocation, shouldValidateSchema);
225325
}
226326
return validationMessages;
227327
}
@@ -237,36 +337,14 @@ private static JsonNode getDefaultNode(JsonSchema schema) {
237337
return result;
238338
}
239339

240-
private void doWalk(ExecutionContext executionContext, HashSet<ValidationMessage> validationMessages, int i, JsonNode node,
241-
JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
242-
if (this.schema != null) {
243-
// Walk the schema.
244-
walkSchema(executionContext, this.schema, node, rootNode, instanceLocation.append(i), shouldValidateSchema, validationMessages);
245-
}
246-
247-
if (this.tupleSchema != null) {
248-
if (i < this.tupleSchema.size()) {
249-
// walk tuple schema
250-
walkSchema(executionContext, this.tupleSchema.get(i), node, rootNode, instanceLocation.append(i),
251-
shouldValidateSchema, validationMessages);
252-
} else {
253-
if (this.additionalSchema != null) {
254-
// walk additional item schema
255-
walkSchema(executionContext, this.additionalSchema, node, rootNode, instanceLocation.append(i),
256-
shouldValidateSchema, validationMessages);
257-
}
258-
}
259-
}
260-
}
261-
262340
private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode,
263-
JsonNodePath instanceLocation, boolean shouldValidateSchema, Set<ValidationMessage> validationMessages) {
264-
boolean executeWalk = this.validationContext.getConfig().getItemWalkListenerRunner().runPreWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(),
341+
JsonNodePath instanceLocation, boolean shouldValidateSchema, Set<ValidationMessage> validationMessages, String keyword) {
342+
boolean executeWalk = this.validationContext.getConfig().getItemWalkListenerRunner().runPreWalkListeners(executionContext, keyword,
265343
node, rootNode, instanceLocation, walkSchema, this);
266344
if (executeWalk) {
267345
validationMessages.addAll(walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema));
268346
}
269-
this.validationContext.getConfig().getItemWalkListenerRunner().runPostWalkListeners(executionContext, ValidatorTypeCode.ITEMS.getValue(), node, rootNode,
347+
this.validationContext.getConfig().getItemWalkListenerRunner().runPostWalkListeners(executionContext, keyword, node, rootNode,
270348
instanceLocation, walkSchema, this, validationMessages);
271349

272350
}

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode n
122122
&& this.schema != null) {
123123
defaultNode = getDefaultNode(this.schema);
124124
}
125+
boolean evaluated = false;
125126
for (int i = this.prefixCount; i < node.size(); ++i) {
126127
JsonNode n = node.get(i);
127128
if (n.isNull() && defaultNode != null) {
@@ -131,10 +132,24 @@ public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode n
131132
// Walk the schema.
132133
walkSchema(executionContext, this.schema, n, rootNode, instanceLocation.append(i), shouldValidateSchema,
133134
validationMessages);
135+
if (n != null) {
136+
evaluated = true;
137+
}
138+
}
139+
if (evaluated) {
140+
if (collectAnnotations() || collectAnnotations(executionContext)) {
141+
// Applies to all
142+
executionContext.getAnnotations()
143+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
144+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
145+
.keyword(getKeyword()).value(true).build());
146+
}
134147
}
135148
} else {
136-
walkSchema(executionContext, this.schema, node, rootNode, instanceLocation, shouldValidateSchema,
137-
validationMessages);
149+
// If the node is not an ArrayNode, eg. ObjectNode or null then the instance is null.
150+
// The instance location starts at the end of the prefix count.
151+
walkSchema(executionContext, this.schema, null, rootNode, instanceLocation.append(this.prefixCount),
152+
shouldValidateSchema, validationMessages);
138153
}
139154

140155
return validationMessages;
@@ -150,7 +165,7 @@ private static JsonNode getDefaultNode(JsonSchema schema) {
150165
}
151166
return result;
152167
}
153-
168+
154169
private void walkSchema(ExecutionContext executionContext, JsonSchema walkSchema, JsonNode node, JsonNode rootNode,
155170
JsonNodePath instanceLocation, boolean shouldValidateSchema, Set<ValidationMessage> validationMessages) {
156171
//@formatter:off

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ default void preloadJsonSchema() throws JsonSchemaException {
5959
@Override
6060
default Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
6161
JsonNodePath instanceLocation, boolean shouldValidateSchema) {
62+
if (node == null) {
63+
// Note that null is not the same as NullNode
64+
return Collections.emptySet();
65+
}
6266
return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation)
6367
: Collections.emptySet();
6468
}

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

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ public PrefixItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluati
4747
this.tupleSchema = new ArrayList<>();
4848

4949
if (schemaNode instanceof ArrayNode && 0 < schemaNode.size()) {
50+
int i = 0;
5051
for (JsonNode s : schemaNode) {
51-
this.tupleSchema.add(validationContext.newSchema(schemaLocation, evaluationPath, s, parentSchema));
52+
this.tupleSchema.add(validationContext.newSchema(schemaLocation.append(i), evaluationPath.append(i), s,
53+
parentSchema));
54+
i++;
5255
}
5356
} else {
5457
throw new IllegalArgumentException("The value of 'prefixItems' MUST be a non-empty array of valid JSON Schemas.");
@@ -101,22 +104,49 @@ public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNo
101104
@Override
102105
public Set<ValidationMessage> walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) {
103106
Set<ValidationMessage> validationMessages = new LinkedHashSet<>();
104-
105-
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()
106-
&& node.isArray()) {
107+
if (node instanceof ArrayNode) {
107108
ArrayNode array = (ArrayNode) node;
108-
int count = Math.min(node.size(), this.tupleSchema.size());
109+
int count = this.tupleSchema.size();
109110
for (int i = 0; i < count; ++i) {
110111
JsonNode n = node.get(i);
111-
JsonNode defaultNode = getDefaultNode(this.tupleSchema.get(i));
112-
if (n.isNull() && defaultNode != null) {
113-
array.set(i, defaultNode);
114-
n = defaultNode;
112+
if (this.validationContext.getConfig().getApplyDefaultsStrategy().shouldApplyArrayDefaults()) {
113+
JsonNode defaultNode = getDefaultNode(this.tupleSchema.get(i));
114+
if (n != null) {
115+
// Defaults only set if array index is explicitly null
116+
if (n.isNull() && defaultNode != null) {
117+
array.set(i, defaultNode);
118+
n = defaultNode;
119+
}
120+
}
115121
}
116122
doWalk(executionContext, validationMessages, i, n, rootNode, instanceLocation, shouldValidateSchema);
117123
}
118-
}
119124

125+
// Add annotation
126+
if (collectAnnotations() || collectAnnotations(executionContext)) {
127+
// Tuples
128+
int items = node.isArray() ? node.size() : 1;
129+
int schemas = this.tupleSchema.size();
130+
if (items > schemas) {
131+
// More items than schemas so the keyword only applied to the number of schemas
132+
executionContext.getAnnotations()
133+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
134+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
135+
.keyword(getKeyword()).value(schemas).build());
136+
} else {
137+
// Applies to all
138+
executionContext.getAnnotations()
139+
.put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation)
140+
.evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation)
141+
.keyword(getKeyword()).value(true).build());
142+
}
143+
}
144+
} else {
145+
int count = this.tupleSchema.size();
146+
for (int i = 0; i < count; ++i) {
147+
doWalk(executionContext, validationMessages, i, null, rootNode, instanceLocation, shouldValidateSchema);
148+
}
149+
}
120150
return validationMessages;
121151
}
122152

0 commit comments

Comments
 (0)