Skip to content

Commit 880329b

Browse files
authored
Allow tracking of json node location information (#1046)
1 parent 4045e7e commit 880329b

27 files changed

+1567
-26
lines changed

README.md

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,8 @@ Assertions contains the following additional information
341341
| Arguments | The arguments used for generating the message.
342342
| Type | The keyword that generated the message.
343343
| Property | The property name that caused the validation error for example for the `required` keyword. Note that this is not part of the instance location as that points to the instance node.
344-
| Schema Node | The `JsonNode` pointed to by the Schema Location.
345-
| Instance Node | The `JsonNode` pointed to by the Instance Location.
344+
| Schema Node | The `JsonNode` pointed to by the Schema Location. This is the schema data that caused the input data to fail. It is possible to get the location information by configuring the `JsonSchemaFactory` with the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(schemaNode)`.
345+
| Instance Node | The `JsonNode` pointed to by the Instance Location. This is the input data that failed validation. It is possible to get the location information by configuring the `JsonSchemaFactory` with the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(instanceNode)`.
346346
| Details | Additional details that can be set by custom keyword validator implementations. This is not used by the library.
347347

348348
Annotations contains the following additional information
@@ -351,6 +351,58 @@ Annotations contains the following additional information
351351
|-------------------|-------------------
352352
| Value | The annotation value generated
353353

354+
##### Line and Column Information
355+
356+
The library can be configured to store line and column information in the `JsonNode` instances for the instance and schema nodes. This will adversely affect performance and is not configured by default.
357+
358+
This is done by configuring a `LocationJsonNodeFactoryFactory` on the `JsonSchemaFactory`. The `JsonLocation` information can then be retrieved using `JsonNodes.tokenLocationOf(jsonNode)`.
359+
360+
```java
361+
String schemaData = "{\r\n"
362+
+ " \"$id\": \"https://schema/myschema\",\r\n"
363+
+ " \"properties\": {\r\n"
364+
+ " \"startDate\": {\r\n"
365+
+ " \"format\": \"date\",\r\n"
366+
+ " \"minLength\": 6\r\n"
367+
+ " }\r\n"
368+
+ " }\r\n"
369+
+ "}";
370+
String inputData = "{\r\n"
371+
+ " \"startDate\": \"1\"\r\n"
372+
+ "}";
373+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
374+
builder -> builder.jsonNodeFactoryFactory(LocationJsonNodeFactoryFactory.getInstance()));
375+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
376+
config.setPathType(PathType.JSON_POINTER);
377+
JsonSchema schema = factory.getSchema(schemaData, InputFormat.JSON, config);
378+
Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON, executionContext -> {
379+
executionContext.getExecutionConfig().setFormatAssertionsEnabled(true);
380+
});
381+
List<ValidationMessage> list = messages.stream().collect(Collectors.toList());
382+
ValidationMessage format = list.get(0);
383+
JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode());
384+
JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode());
385+
ValidationMessage minLength = list.get(1);
386+
JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode());
387+
JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode());
388+
389+
assertEquals("format", format.getType());
390+
assertEquals("date", format.getSchemaNode().asText());
391+
assertEquals(5, formatSchemaNodeTokenLocation.getLineNr());
392+
assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr());
393+
assertEquals("1", format.getInstanceNode().asText());
394+
assertEquals(2, formatInstanceNodeTokenLocation.getLineNr());
395+
assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr());
396+
assertEquals("minLength", minLength.getType());
397+
assertEquals("6", minLength.getSchemaNode().asText());
398+
assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr());
399+
assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr());
400+
assertEquals("1", minLength.getInstanceNode().asText());
401+
assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr());
402+
assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr());
403+
assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr());
404+
```
405+
354406

355407
#### Output formats
356408

@@ -479,9 +531,9 @@ The following is sample output from the Hierarchical format.
479531

480532
When the library creates a schema from the schema factory, it creates a distinct validator instance for each location on the evaluation path. This means if there are different `$ref` that reference the same schema location, different validator instances are created for each evaluation path.
481533

482-
When the schema is created, the library will automatically preload all the validators needed and resolve references. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved.
534+
When the schema is created, the library will by default automatically preload all the validators needed and resolve references. This can be disabled with the `preloadJsonSchema` option in the `SchemaValidatorsConfig`. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved.
483535

484-
The `JsonSchema` created from the factory should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved.
536+
Instances for `JsonSchemaFactory` and the `JsonSchema` created from it are designed to be thread-safe provided its configuration is not modified and should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved. When references are resolved, the validators created will be cached. For schemas that have deeply nested references, the memory needed for the validators may be very high, in which case the caching may need to be disabled using the `cacheRefs` option in the `JsonSchemaValidatorsConfig`. Disabling this will mean the validators from the references need to be re-created for each validation run which will impact performance.
485537

486538
Collecting annotations will adversely affect validation performance.
487539

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ public static JsonMetaSchema getV202012() {
369369
* Use {@link #getV4()} for the Draft 4 Metaschema, or if you need a builder based on Draft4, use
370370
*
371371
* <code>
372-
* JsonMetaSchema.builder("http://your-metaschema-iri", JsonMetaSchema.getDraftV4()).build();
372+
* JsonMetaSchema.builder("http://your-metaschema-iri", JsonMetaSchema.getV4()).build();
373373
* </code>
374374
*
375375
* @param iri the IRI of the metaschema that will be defined via this builder.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema;
17+
18+
/**
19+
* JsonNode location information.
20+
*/
21+
public class JsonNodeLocation {
22+
private final int line;
23+
private final int column;
24+
25+
public JsonNodeLocation(int line, int column) {
26+
super();
27+
this.line = line;
28+
this.column = column;
29+
}
30+
31+
public int getLine() {
32+
return line;
33+
}
34+
35+
public int getColumn() {
36+
return column;
37+
}
38+
39+
@Override
40+
public String toString() {
41+
return "JsonNodeLocation [line=" + line + ", column=" + column + "]";
42+
}
43+
}

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,13 @@
1616

1717
package com.networknt.schema;
1818

19-
import com.fasterxml.jackson.core.JsonProcessingException;
2019
import com.fasterxml.jackson.databind.JsonNode;
2120
import com.fasterxml.jackson.databind.node.ObjectNode;
2221
import com.networknt.schema.SpecVersion.VersionFlag;
23-
import com.networknt.schema.serialization.JsonMapperFactory;
24-
import com.networknt.schema.serialization.YamlMapperFactory;
2522
import com.networknt.schema.utils.JsonNodes;
2623
import com.networknt.schema.utils.SetView;
2724

25+
import java.io.IOException;
2826
import java.io.UnsupportedEncodingException;
2927
import java.net.URLDecoder;
3028
import java.util.ArrayList;
@@ -46,6 +44,9 @@
4644
* This is the core of json constraint implementation. It parses json constraint
4745
* file and generates JsonValidators. The class is thread safe, once it is
4846
* constructed, it can be used to validate multiple json data concurrently.
47+
* <p>
48+
* JsonSchema instances are thread-safe provided its configuration is not
49+
* modified.
4950
*/
5051
public class JsonSchema extends BaseJsonValidator {
5152
private static final long V201909_VALUE = VersionFlag.V201909.getVersionFlagValue();
@@ -871,15 +872,10 @@ public <T> T validate(ExecutionContext executionContext, JsonNode node, OutputFo
871872
*/
872873
private JsonNode deserialize(String input, InputFormat inputFormat) {
873874
try {
874-
if (InputFormat.JSON.equals(inputFormat)) {
875-
return JsonMapperFactory.getInstance().readTree(input);
876-
} else if (InputFormat.YAML.equals(inputFormat)) {
877-
return YamlMapperFactory.getInstance().readTree(input);
878-
}
879-
} catch (JsonProcessingException e) {
875+
return this.getValidationContext().getJsonSchemaFactory().readTree(input, inputFormat);
876+
} catch (IOException e) {
880877
throw new IllegalArgumentException("Invalid input", e);
881878
}
882-
throw new IllegalArgumentException("Unsupported input format "+inputFormat);
883879
}
884880

885881
public ValidationResult validateAndCollect(ExecutionContext executionContext, JsonNode node) {

0 commit comments

Comments
 (0)