diff --git a/README.md b/README.md index c9c15a268..7f95eb76a 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,13 @@ [![codecov.io](https://codecov.io/github/networknt/json-schema-validator/coverage.svg?branch=master)](https://codecov.io/github/networknt/json-schema-validator?branch=master) [![Javadocs](http://www.javadoc.io/badge/com.networknt/json-schema-validator.svg)](https://www.javadoc.io/doc/com.networknt/json-schema-validator) -This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](https://json-schema.org/specification) specification for JSON schema validation. This implementation supports [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md). +This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](https://json-schema.org/specification) specification for JSON schema validation. This implementation supports [Customizing Dialects, Vocabularies, Keywords and Formats](doc/custom-dialect.md). -In addition, [OpenAPI](doc/openapi.md) 3 request/response validation is supported with the use of the appropriate meta-schema. For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. +The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. + +[OpenAPI](doc/openapi.md) 3 request/response validation is supported with the use of the appropriate dialect. + +As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. ## JSON Schema Specification compatibility @@ -28,7 +32,7 @@ Information on the compatibility support for each version, including known issue Since [Draft 2019-09](https://json-schema.org/draft/2019-09/json-schema-validation#rfc.section.7) the `format` keyword only generates annotations by default and does not generate assertions. -This behavior can be overridden to generate assertions by setting the `setFormatAssertionsEnabled` to `true` in `SchemaValidatorsConfig` or `ExecutionConfig`. +This behavior can be overridden to generate assertions by setting the `formatAssertionsEnabled` to `true` in `SchemaRegistryConfig` or `ExecutionConfig`. ## Upgrading to new versions @@ -41,8 +45,9 @@ The [Releases](https://github.com/networknt/json-schema-validator/releases) page ## Comparing against other implementations The [JSON Schema Validation Comparison](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. -* [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional#summary-results-table) -* [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) + +- [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional#summary-results-table) +- [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report](https://bowtie.report/) that compares functional characteristics of different implementations, including non-Java implementations, but does not do any performance benchmarking. @@ -50,47 +55,47 @@ The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report #### Performance -This should be the fastest Java JSON Schema Validator implementation. - The following is the benchmark results from the [JSON Schema Validator Perftest](https://github.com/networknt/json-schema-validator-perftest) project that uses the [Java Microbenchmark Harness](https://github.com/openjdk/jmh). Note that the benchmark results are highly dependent on the input data workloads and schemas used for the validation. -In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) for benchmark results for more typical workloads +In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) for benchmark results that use the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). If performance is an important consideration, the specific sample workloads should be benchmarked, as there are different performance characteristics when certain keywords are used. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators, and annotation collection will adversely affect performance. -##### NetworkNT 1.4.1 +Special attention should also be made for inefficient schemas using deeply nested `oneOf` or `anyOf` that do not have a condition to short-circuit the evaluation using `if` and `then`. The validator has no choice but to perform all the evaluations, and the error messages would be typically very confusing as it will return all the messages from the children. + +##### NetworkNT 2.0.0 ``` -Benchmark Mode Cnt Score Error Units -NetworkntBenchmark.testValidate thrpt 10 8352.126 ± 61.870 ops/s -NetworkntBenchmark.testValidate:gc.alloc.rate thrpt 10 721.296 ± 5.342 MB/sec -NetworkntBenchmark.testValidate:gc.alloc.rate.norm thrpt 10 90560.013 ± 0.001 B/op -NetworkntBenchmark.testValidate:gc.count thrpt 10 61.000 counts -NetworkntBenchmark.testValidate:gc.time thrpt 10 68.000 ms +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.basic thrpt 10 5297.105 ± 290.078 ops/s +NetworkntBenchmark.basic:gc.alloc.rate thrpt 10 1618.328 ± 88.626 MB/sec +NetworkntBenchmark.basic:gc.alloc.rate.norm thrpt 10 320360.020 ± 0.002 B/op +NetworkntBenchmark.basic:gc.count thrpt 10 365.000 counts +NetworkntBenchmark.basic:gc.time thrpt 10 130.000 ms ``` -###### Everit 1.14.1 +###### Everit 1.14.6 ``` -Benchmark Mode Cnt Score Error Units -EveritBenchmark.testValidate thrpt 10 3775.453 ± 44.023 ops/s -EveritBenchmark.testValidate:gc.alloc.rate thrpt 10 1667.345 ± 19.437 MB/sec -EveritBenchmark.testValidate:gc.alloc.rate.norm thrpt 10 463104.030 ± 0.003 B/op -EveritBenchmark.testValidate:gc.count thrpt 10 140.000 counts -EveritBenchmark.testValidate:gc.time thrpt 10 158.000 ms +Benchmark Mode Cnt Score Error Units +EveritBenchmark.basic thrpt 10 4615.637 ± 151.195 ops/s +EveritBenchmark.basic:gc.alloc.rate thrpt 10 2097.810 ± 68.708 MB/sec +EveritBenchmark.basic:gc.alloc.rate.norm thrpt 10 476592.023 ± 0.001 B/op +EveritBenchmark.basic:gc.count thrpt 10 521.000 counts +EveritBenchmark.basic:gc.time thrpt 10 170.000 ms ``` #### Functionality This implementation is tested against the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). As tests are continually added to the suite, these test results may not be current. -| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | -|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| -| NetworkNt | pass: r:4803 (100.0%) o:2372 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:822 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:906 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1220 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1245 (100.0%) o:637 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +| --------------- | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| NetworkNt | pass: r:4840 (100.0%) o:2421 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:255 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:829 (100.0%) o:322 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:913 (100.0%) o:554 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1227 (100.0%) o:639 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1261 (100.0%) o:651 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | -* Note that this uses the `JoniRegularExpressionFactory` for the `pattern` and `format` `regex` tests. +- Note that this uses the `JoniRegularExpressionFactory` for the `pattern` and `format` `regex` tests. #### Jackson Parser @@ -102,7 +107,7 @@ The library works with JSON and YAML on both schema definitions and input data. #### OpenAPI Support -The OpenAPI 3.0 specification is using JSON schema to validate the request/response, but there are some differences. With a configuration file, you can enable the library to work with OpenAPI 3.0 validation. +The OpenAPI 3.0 specification is using JSON schema to validate the request/response. The library has support for the OpenAPI 3.0 and OpenAPI 3.1 dialects. #### Minimal Dependencies @@ -222,7 +227,7 @@ This package is available on Maven central. com.networknt json-schema-validator - 1.5.9 + 2.0.0 ``` @@ -230,7 +235,7 @@ This package is available on Maven central. ```java dependencies { - implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.5.9'); + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '2.0.0'); } ``` @@ -238,67 +243,84 @@ dependencies { The following example demonstrates how inputs are validated against a schema. It comprises the following steps. -* Creating a schema factory with the default schema dialect and how the schemas can be retrieved. - * Configuring mapping the `$id` to a retrieval URI using `schemaMappers`. - * Configuring how the schemas are loaded using the retrieval URI using `schemaLoaders`. - For instance a `Map schemas` containing a mapping of retrieval URI to schema data as a `String` can by configured using `builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas))`. This also accepts a `Function schemaRetrievalFunction`. -* Creating a configuration for controlling validator behavior. -* Loading a schema from a schema location along with the validator configuration. -* Using the schema to validate the data along with setting any execution specific configuration like for instance the locale or whether format assertions are enabled. +- Creating a configuration for controlling validator behavior. +- Creating a schema registry with the default schema dialect and how the schemas can be retrieved. + - Configuring mapping the `$id` to a retrieval IRI using `schemaIdResolvers`. + - Configuring how the schemas are loaded using the retrieval IRI. + For instance a `Map schemas` containing a mapping of retrieval URI to schema data as a `String` can by configured using `builder.schemas(schemas)`. This also accepts a `Function schemaRetrievalFunction`. +- Loading a schema from a schema location. +- Using the schema to validate the data along with setting any execution specific configuration like for instance the locale or whether format assertions are enabled. ```java -// This creates a schema factory that will use Draft 2020-12 as the default if $schema is not specified -// in the schema data. If $schema is specified in the schema data then that schema dialect will be used -// instead and this version is ignored. -JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> - // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath:schema/ - builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")) -); - -SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); -// By default the JDK regular expression implementation which is not ECMA 262 compliant is used -// Note that setting this requires including optional dependencies -// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); -// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); -SchemaValidatorsConfig config = builder.build(); - -// Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. -// If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. -JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); +/* + * The SchemaRegistryConfig can be optionally used to configure certain aspects + * of how the validation is performed. + * + * By default the JDK regular expression implementation which is not ECMA 262 + * compliant is used. The GraalJSRegularExpressionFactory.getInstance() offers + * the best compliance followed by JoniRegularExpressionFactory.getInstance() + * but both require additional optional dependencies. + */ +SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(JoniRegularExpressionFactory.getInstance()).build(); + +/* + * This creates a schema registry that supports all the standard dialects for + * cross-dialect validation and will use Draft 2020-12 as the default if $schema + * is not specified in the schema data. If $schema is specified in the schema + * data then that schema dialect will be used instead and this version is + * ignored. + */ +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12, + builder -> builder.schemaRegistryConfig(schemaRegistryConfig) + /* + * This creates a mapping from $id which starts with + * https://www.example.org/schema to the retrieval IRI classpath:schema. + */ + .schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + .mapPrefix("https://www.example.com/schema", "classpath:schema"))); + +/* + * Due to the mapping the schema will be retrieved from the classpath at + * classpath:schema/example-main.json. If the schema data does not specify an + * $id the absolute IRI of the schema location will be used as the $id. If the + * schema data does not specify a dialect using $schema the default dialect + * specified when creating the schema registry. + */ +Schema schema = schemaRegistry.getSchema(SchemaLocation.of("https://www.example.com/schema/example-main.json")); String input = "{\r\n" - + " \"main\": {\r\n" - + " \"common\": {\r\n" - + " \"field\": \"invalidfield\"\r\n" - + " }\r\n" - + " }\r\n" - + "}"; - -Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { - // By default since Draft 2019-09 the format keyword only generates annotations and not assertions - executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + + " \"main\": {\r\n" + + " \"common\": {\r\n" + + " \"field\": \"invalidfield\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + +List errors = schema.validate(input, InputFormat.JSON, executionContext -> { + /* + * By default since Draft 2019-09 the format keyword only generates annotations + * and not assertions. + */ + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); }); ``` ### Validating a schema against a meta-schema -The following example demonstrates how a schema is validated against a meta-schema. +The following example demonstrates how a schema is validated against a meta-schema of a dialect. This is actually the same as validating inputs against a schema except in this case the input is the schema and the schema used is the meta-schema. -Note that the meta-schemas for Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft 2020-12 are bundled with the library and these classpath resources will be used by default. +Note that the meta-schemas for Draft 4, Draft 6, Draft 7, Draft 201 +9-09 and Draft 2020-12 are bundled with the library and these classpath resources will be used by default. ```java -JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012); - -SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); -// By default the JDK regular expression implementation which is not ECMA 262 compliant is used -// Note that setting this requires including optional dependencies -// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); -// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); -SchemaValidatorsConfig config = builder.build(); - -// Due to the mapping the meta-schema will be retrieved from the classpath at classpath:draft/2020-12/schema. -JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V202012), config); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +/* + * Due to the mapping the meta-schema for the dialect will be retrieved from the + * classpath at classpath:draft/2020-12/schema. + */ +Schema schema = schemaRegistry.getSchema(SchemaLocation.of(Dialects.getDraft202012().getId())); String input = "{\r\n" + " \"type\": \"object\",\r\n" + " \"properties\": {\r\n" @@ -308,56 +330,58 @@ String input = "{\r\n" + " }\r\n" + " }\r\n" + "}"; -Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { - // By default since Draft 2019-09 the format keyword only generates annotations and not assertions - executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +List errors = schema.validate(input, InputFormat.JSON, executionContext -> { + /* + * By default since Draft 2019-09 the format keyword only generates annotations + * and not assertions. + */ + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); }); ``` + ### Results and output formats #### Results The following types of results are generated by the library. -| Type | Description -|-------------|------------------- -| Assertions | Validation errors generated by a keyword on a particular input data instance. This is generally described in a `ValidationMessage` or in a `OutputUnit`. Note that since Draft 2019-09 the `format` keyword no longer generates assertions by default and instead generates only annotations unless configured otherwise using a configuration option or by using a meta-schema that uses the appropriate vocabulary. -| Annotations | Additional information generated by a keyword for a particular input data instance. This is generally described in a `OutputUnit`. Annotation collection and reporting is turned off by default. Annotations required by keywords such as `unevaluatedProperties` or `unevaluatedItems` are always collected for evaluation purposes and cannot be disabled but will not be reported unless configured to do so. +| Type | Description | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Assertions | Validation errors generated by an assertion keyword on a particular input data instance. This is generally described in an `Error` or in a `OutputUnit`. Note that since Draft 2019-09 the `format` keyword no longer generates assertions by default and instead generates only annotations unless configured otherwise using a configuration option or by using a dialect that uses the appropriate vocabulary. | +| Annotations | Additional information generated by an annotation keyword for a particular input data instance. This is generally described in a `OutputUnit`. Annotation collection and reporting is turned off by default. Annotations required by keywords such as `unevaluatedProperties` or `unevaluatedItems` are always collected for evaluation purposes and cannot be disabled but will not be reported unless configured to do so. | The following information is used to describe both types of results. -| Type | Description -|-------------------|------------------- -| Evaluation Path | This is the set of keys from the root through which evaluation passes to reach the schema for evaluating the instance. This includes `$ref` and `$dynamicRef`. eg. ```/properties/bar/$ref/properties/bar-prop``` -| Schema Location | This is the canonical IRI of the schema plus the JSON pointer fragment to the schema that was used for evaluating the instance. eg. ```https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop``` -| Instance Location | This is the JSON pointer fragment to the instance data that was being evaluated. eg. ```/bar/bar-prop``` +| Type | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Evaluation Path | This is the set of keys from the root through which evaluation passes to reach the schema for evaluating the instance. This includes `$ref` and `$dynamicRef`. eg. `/properties/bar/$ref/properties/bar-prop` | +| Schema Location | This is the canonical IRI of the schema plus the JSON pointer fragment to the schema that was used for evaluating the instance. eg. `https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop` | +| Instance Location | This is the JSON pointer fragment to the instance data that was being evaluated. eg. `/bar/bar-prop` | Assertions contains the following additional information -| Type | Description -|-------------------|------------------- -| Message | The validation error message. -| Code | The error code. -| Message Key | The message key used for generating the message for localization. -| Arguments | The arguments used for generating the message. -| Type | The keyword that generated the message. -| 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. -| 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 a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(schemaNode)`. -| 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 a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(instanceNode)`. -| Error | The error. -| Details | Additional details that can be set by custom keyword validator implementations. This is not used by the library. +| Type | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Message | The validation error message. | +| Message Key | The message key used for generating the message for localization. | +| Arguments | The arguments used for generating the message. | +| Keyword | The keyword that generated the message. | +| 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 `SchemaRegistry` with a `NodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenStreamLocationOf(schemaNode)`. | +| 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 `SchemaRegistry` with a `NodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenStreamLocationOf(instanceNode)`. | +| Error | The error. | +| Details | Additional details that can be set by custom keyword validator implementations. The library will set the `property` and `index` details for certain errors. For instane the `required` keyword will set the `property`. Note that this is not part of the instance location as that points to the instance node. | Annotations contains the following additional information -| Type | Description -|-------------------|------------------- -| Value | The annotation value generated +| Type | Description | +| ----- | ------------------------------ | +| Value | The annotation value generated | ##### Line and Column Information 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. -This is done by configuring a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory`on the `JsonSchemaFactory`. The `JsonLocation` information can then be retrieved using `JsonNodes.tokenLocationOf(jsonNode)`. +This is done by configuring a `NodeReader` that uses the `LocationJsonNodeFactoryFactory` on the `SchemaRegistry`. The `JsonLocation` information can then be retrieved using `JsonNodes.tokenStreamLocationOf(jsonNode)`. ```java String schemaData = "{\r\n" @@ -372,29 +396,28 @@ String schemaData = "{\r\n" String inputData = "{\r\n" + " \"startDate\": \"1\"\r\n" + "}"; -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, - builder -> builder.jsonNodeReader(JsonNodeReader.builder().locationAware().build())); -SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); -JsonSchema schema = factory.getSchema(schemaData, InputFormat.JSON, config); -Set messages = schema.validate(inputData, InputFormat.JSON, executionContext -> { - executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.nodeReader(nodeReader -> nodeReader.locationAware())); + +Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON); +List errors = schema.validate(inputData, InputFormat.JSON, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); }); -List list = messages.stream().collect(Collectors.toList()); -ValidationMessage format = list.get(0); -JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode()); -JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode()); -ValidationMessage minLength = list.get(1); -JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode()); -JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode()); - -assertEquals("format", format.getType()); +Error format = errors.get(0); +JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenStreamLocationOf(format.getInstanceNode()); +JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenStreamLocationOf(format.getSchemaNode()); +Error minLength = errors.get(1); +JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenStreamLocationOf(minLength.getInstanceNode()); +JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenStreamLocationOf(minLength.getSchemaNode()); + +assertEquals("format", format.getKeyword()); assertEquals("date", format.getSchemaNode().asText()); assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr()); assertEquals("1", format.getInstanceNode().asText()); assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr()); -assertEquals("minLength", minLength.getType()); +assertEquals("minLength", minLength.getKeyword()); assertEquals("6", minLength.getSchemaNode().asText()); assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr()); @@ -404,97 +427,106 @@ assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); ``` - #### Output formats This library implements the Flag, List and Hierarchical output formats defined in the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/8270653a9f59fadd2df0d789f22d486254505bbe/jsonschema-validation-output-machines.md). The List and Hierarchical output formats are particularly helpful for understanding how the system arrived at a particular result. -| Output Format | Description -|-------------------|------------------- -| Default | Generates the list of assertions. -| Boolean | Returns `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. -| Flag | Returns an `OutputFlag` object with `valid` having `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. -| List | Returns an `OutputUnit` object with `details` with a list of `OutputUnit` objects with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. -| Hierarchical | Returns an `OutputUnit` object with a hierarchy of `OutputUnit` objects for the evaluation path with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. +| Output Format | Description | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Default | Generates the list of assertions. | +| Boolean | Returns `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. | +| Flag | Returns an `OutputFlag` object with `valid` having `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. | +| List | Returns an `OutputUnit` object with `details` with a list of `OutputUnit` objects with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. | +| Hierarchical | Returns an `OutputUnit` object with a hierarchy of `OutputUnit` objects for the evaluation path with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. | The following example shows how to generate the hierarchical output format with annotation collection and reporting turned on and format assertions turned on. ```java -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); -SchemaValidatorsConfig config = SchemaValidatorsConfig().builder().formatAssertionsEnabled(true).build(); -JsonSchema schema = factory.getSchema(SchemaLocation.of("https://json-schema.org/schemas/example"), config); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +Schema schema = schemaRegistry.getSchema(SchemaLocation.of("https://json-schema.org/schemas/example")); OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { - executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true); - executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); + executionContext.executionConfig(executionConfig -> executionConfig + .annotationCollectionEnabled(true) + .annotationCollectionFilter(keyword -> true) + .formatAssertionsEnabled(true)); }); ``` + The following is sample output from the Hierarchical format. ```json { - "valid" : false, - "evaluationPath" : "", - "schemaLocation" : "https://json-schema.org/schemas/example#", - "instanceLocation" : "", - "droppedAnnotations" : { - "properties" : [ "foo", "bar" ], - "title" : "root" + "valid": false, + "evaluationPath": "", + "schemaLocation": "https://json-schema.org/schemas/example#", + "instanceLocation": "", + "droppedAnnotations": { + "properties": ["foo", "bar"], + "title": "root" }, - "details" : [ { - "valid" : false, - "evaluationPath" : "/properties/foo/allOf/0", - "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/0", - "instanceLocation" : "/foo", - "errors" : { - "required" : "required property 'unspecified-prop' not found" - } - }, { - "valid" : false, - "evaluationPath" : "/properties/foo/allOf/1", - "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1", - "instanceLocation" : "/foo", - "droppedAnnotations" : { - "properties" : [ "foo-prop" ], - "title" : "foo-title", - "additionalProperties" : [ "foo-prop", "other-prop" ] + "details": [ + { + "valid": false, + "evaluationPath": "/properties/foo/allOf/0", + "schemaLocation": "https://json-schema.org/schemas/example#/properties/foo/allOf/0", + "instanceLocation": "/foo", + "errors": { + "required": "required property 'unspecified-prop' not found" + } }, - "details" : [ { - "valid" : false, - "evaluationPath" : "/properties/foo/allOf/1/properties/foo-prop", - "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop", - "instanceLocation" : "/foo/foo-prop", - "errors" : { - "const" : "must be a constant value 1" + { + "valid": false, + "evaluationPath": "/properties/foo/allOf/1", + "schemaLocation": "https://json-schema.org/schemas/example#/properties/foo/allOf/1", + "instanceLocation": "/foo", + "droppedAnnotations": { + "properties": ["foo-prop"], + "title": "foo-title", + "additionalProperties": ["foo-prop", "other-prop"] }, - "droppedAnnotations" : { - "title" : "foo-prop-title" - } - } ] - }, { - "valid" : false, - "evaluationPath" : "/properties/bar/$ref", - "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar", - "instanceLocation" : "/bar", - "droppedAnnotations" : { - "properties" : [ "bar-prop" ], - "title" : "bar-title" + "details": [ + { + "valid": false, + "evaluationPath": "/properties/foo/allOf/1/properties/foo-prop", + "schemaLocation": "https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop", + "instanceLocation": "/foo/foo-prop", + "errors": { + "const": "must be a constant value 1" + }, + "droppedAnnotations": { + "title": "foo-prop-title" + } + } + ] }, - "details" : [ { - "valid" : false, - "evaluationPath" : "/properties/bar/$ref/properties/bar-prop", - "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop", - "instanceLocation" : "/bar/bar-prop", - "errors" : { - "minimum" : "must have a minimum value of 10" + { + "valid": false, + "evaluationPath": "/properties/bar/$ref", + "schemaLocation": "https://json-schema.org/schemas/example#/$defs/bar", + "instanceLocation": "/bar", + "droppedAnnotations": { + "properties": ["bar-prop"], + "title": "bar-title" }, - "droppedAnnotations" : { - "title" : "bar-prop-title" - } - } ] - } ] + "details": [ + { + "valid": false, + "evaluationPath": "/properties/bar/$ref/properties/bar-prop", + "schemaLocation": "https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop", + "instanceLocation": "/bar/bar-prop", + "errors": { + "minimum": "must have a minimum value of 10" + }, + "droppedAnnotations": { + "title": "bar-prop-title" + } + } + ] + } + ] } ``` @@ -502,103 +534,92 @@ The following is sample output from the Hierarchical format. ### Execution Configuration -| Name | Description | Default Value -|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- -| `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` -| `annotationCollectionFilter` | The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` -| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` -| `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaValidatorsConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` -| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` -| `debugEnabled` | Controls whether debug logging is enabled for logging the node information when processing. Note that this will generate a lot of logs that will affect performance. | `false` - -### Schema Validators Configuration - -| Name | Description | Default Value -|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- -| `applyDefaultsStrategy` | The strategy for applying defaults when walking when missing or null nodes are encountered. | `ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY` -| `cacheRefs` | Whether the schemas loaded from refs will be cached and reused for subsequent runs. Setting this to `false` will affect performance but may be neccessary to prevent high memory usage for the cache if multiple nested applicators like `anyOf`, `oneOf` and `allOf` are used. | `true` -| `discriminatorKeywordEnabled` | Whether the `discriminator` keyword is handled according to OpenAPI 3. | `false` -| `errorMessageKeyword` | The keyword to use for custom error messages in the schema. If not set this features is disabled. This is typically set to `errorMessage` or `message`. | `null` -| `executionContextCustomizer` | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run. | `null` -| `failFast` | Whether to return failure immediately when an assertion is generated. | `false` -| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` -| `javaSemantics` | Whether java semantics is used for the `type` keyword. | `false` -| `locale` | The locale to use for generating messages in the `ValidationMessage`. | `Locale.getDefault()` -| `losslessNarrowing` | Whether lossless narrowing is used for the `type` keyword. | `false` -| `messageSource` | This is used to retrieve the locale specific messages. | `DefaultMessageSource.getInstance()` -| `nullableKeywordEnabled` | Whether the `nullable` keyword is handled according to OpenAPI 3.0. This affects the `enum` and `type` keywords. | `false` -| `pathType` | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_PATH` to use JSON Path. | `PathType.JSON_POINTER` -| `preloadJsonSchema` | Whether the schema will be preloaded before processing any input. This will use memory but the execution of the validation will be faster. | `true` -| `preloadJsonSchemaRefMaxNestingDepth` | The max depth of the evaluation path to preload when preloading refs. | `40` -| `readOnly` | Whether schema is read only. This affects the `readOnly` keyword. | `null` -| `regularExpressionFactory` | The factory to use to create regular expressions for instance `JoniRegularExpressionFactory` or `GraalJSRegularExpressionFactory`. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown. | `JDKRegularExpressionFactory.getInstance()` -| `schemaIdValidator` | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema. | `JsonSchemaIdValidator.DEFAULT` -| `strict` | This is set whether keywords are strict in their validation. What this does depends on the individual validators. | -| `typeLoose` | Whether types are interpreted in a loose manner. If set to true, a single value can be interpreted as a size 1 array. Strings may also be interpreted as number, integer or boolean. | `false` -| `writeOnly` | Whether schema is write only. This affects the `writeOnly` keyword. | `null` +| Name | Description | Default Value | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` | +| `annotationCollectionFilter` | The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` | +| `locale` | The locale to use for generating messages in `Error`. Note that this value is copied from `SchemaRegistryConfig` for each execution. | `Locale.getDefault()` | +| `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaRegistryConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` | +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` | +| `readOnly` | Used to indicate that the property should not be sent as part of the request payload, but only in the response payload. This affects the `readOnly` keyword used for the OpenAPI dialect. | `null` | +| `writeOnly` | Used to indicate that the property should not be sent as part of the response payload, but only in the request payload. This affects the `writeOnly` keyword used for the OpenAPI dialect. | `null` | + +### Schema Registry Configuration + +| Name | Description | Default Value | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| `cacheRefs` | Whether the schemas loaded from refs will be cached and reused for subsequent runs. Setting this to `false` will affect performance. | `true` | +| `errorMessageKeyword` | The keyword to use for custom error messages in the schema. If not set this features is disabled. This is typically set to `errorMessage` or `message`. | `null` | +| `executionContextCustomizer` | This can be used to customize the `ExecutionContext` generated by the `Schema` for each validation run. | `null` | +| `failFast` | Whether to return failure immediately when an assertion is generated. | `false` | +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` | +| `locale` | The locale to use for generating messages in `Error`. | `Locale.getDefault()` | +| `losslessNarrowing` | Whether lossless narrowing is used for the `type` keyword. Since Draft 6 a value of `1.0` is interpreted as an integer whether or not this is enabled. | `false` | +| `messageSource` | This is used to retrieve the locale specific messages. | `DefaultMessageSource.getInstance()` | +| `pathType` | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_PATH` to use JSON Path. | `PathType.JSON_POINTER` | +| `preloadSchema` | Whether the schema will be preloaded before processing any input. This will use memory but the execution of the validation will be faster. | `true` | +| `regularExpressionFactory` | The factory to use to create regular expressions for instance `JoniRegularExpressionFactory` or `GraalJSRegularExpressionFactory`. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown. | `JDKRegularExpressionFactory.getInstance()` | +| `schemaIdValidator` | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema. | `JsonSchemaIdValidator.DEFAULT` | +| `strict` | This is set whether keywords are strict in their validation. What this does depends on the individual validators. | | +| `typeLoose` | Whether types are interpreted in a loose manner. If set to true, a single value can be interpreted as a size 1 array. Strings may also be interpreted as number, integer or boolean. | `false` | + +### Walk Configuration + +| Name | Description | Default Value | +| ---------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------- | +| `applyDefaultsStrategy` | The strategy for applying defaults when walking when missing or null nodes are encountered. | `ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY` | +| `keywordWalkHandler` | The `WalkHandler` triggered for keywords. | `NoOpWalkHandler.getInstance()` | +| `propertyWalkHandler` | The `WalkHandler` triggered for properties. | `NoOpWalkHandler.getInstance()` | +| `itemWalkHandler` | The `WalkHandler` triggered for items. | `NoOpWalkHandler.getInstance()` | ## Performance Considerations -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. - -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. +Special attention should be made for inefficient schemas using deeply nested `oneOf` or `anyOf` that do not have a condition to short-circuit the evaluation using `if` and `then`. The validator has no choice but to perform all the evaluations, and the error messages would be typically very confusing as it will return all the messages from the children. -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 `SchemaValidatorsConfig`. Disabling this will mean the validators from the references need to be re-created for each validation run which will impact performance. +Instances for `SchemaRegistry` and the `Schema` created from it are designed to be thread-safe provided its configuration is not modified and should be cached and reused. Not reusing the `Schema` 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. Collecting annotations will adversely affect validation performance. The earlier draft specifications contain less keywords that can potentially impact performance. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators. -This does not mean that using a schema with a later draft specification will automatically cause a performance impact. For instance, the `properties` validator will perform checks to determine if annotations need to be collected, and checks if the meta-schema contains the `unevaluatedProperties` keyword and whether the `unevaluatedProperties` keyword exists adjacent the evaluation path. +This does not mean that using a schema with a later draft specification will automatically cause a performance impact. For instance, the `properties` validator will perform checks to determine if annotations need to be collected, and checks if the dialect contains the `unevaluatedProperties` keyword and whether the `unevaluatedProperties` keyword exists adjacent the evaluation path. ## Security Considerations The library assumes that the schemas being loaded are trusted. This security model assumes the use case where the schemas are bundled with the application on the classpath. -| Issue | Description | Mitigation -|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------- -| Schema Loading | The library by default will load schemas from the classpath and over the internet if needed. | A `DisallowSchemaLoader` can be configured to not allow schema retrieval. Alternatively an `AllowSchemaLoader` can be configured to restrict the retrieval IRIs that are allowed. -| Schema Caching | The library by default preloads and caches references when loading schemas. While there is a max nesting depth when preloading schemas it is still possible to construct a schema that has a fan out that consumes a lot of memory from the server. | Set `cacheRefs` option in `SchemaValidatorsConfig` to false. -| Regular Expressions | The library does not validate if a given regular expression is susceptable to denial of service ([ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)). | An `AllowRegularExpressionFactory` can be configured to perform validation on the regular expressions that are allowed. -| Validation Errors | The library by default attempts to return all validation errors. The use of applicators such as `allOf` with a large number of schemas may result in a large number of validation errors taking up memory. | Set `failFast` option in `SchemaValidatorsConfig` to immediately return when the first error is encountered. The `OutputFormat.BOOLEAN` or `OutputFormat.FLAG` also can be used. +| Issue | Description | Mitigation | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Schema Loading | The library by default will load schemas from the classpath and over the internet if needed. | The `SchemaLoader` can be configured to block or allow certain IRIs for schema retrieval. | +| Schema Caching | The library by default preloads and caches references when loading schemas. | Set `cacheRefs` option in `SchemaRegistryConfig` to false. | +| Regular Expressions | The library does not validate if a given regular expression is susceptable to denial of service ([ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)). | An `AllowRegularExpressionFactory` can be configured to perform validation on the regular expressions that are allowed. | +| Validation Errors | The library by default attempts to return all validation errors. The use of applicators such as `allOf` with a large number of schemas may result in a large number of validation errors taking up memory. | Set `failFast` option in `SchemaRegistryConfig` to immediately return when the first error is encountered. The `OutputFormat.BOOLEAN` or `OutputFormat.FLAG` also can be used. | ## [Quick Start](doc/quickstart.md) ## [Customizing Schema Retrieval](doc/schema-retrieval.md) -## [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md) +## [Customizing Dialects, Vocabularies, Keywords and Formats](doc/custom-dialect.md) ## [OpenAPI Specification](doc/openapi.md) -## [Validators](doc/validators.md) - -## [Configuration](doc/config.md) - -## [Specification Version](doc/specversion.md) - -## [YAML Validation](doc/yaml.md) - -## [Collector Context](doc/collector-context.md) - -## [JSON Schema Walkers and WalkListeners](doc/walkers.md) +## [Schema Walkers](doc/walkers.md) ## [Regular Expressions](doc/ecma-262.md) -## [Custom Error Messages](doc/cust-msg.md) +## [Custom Error Messages](doc/error-message.md) ## [Multiple Language](doc/multiple-language.md) ## [MetaSchema Validation](doc/metaschema-validation.md) -## [Validating RFC 3339 durations](doc/duration.md) - ## Projects The [light-rest-4j](https://github.com/networknt/light-rest-4j), [light-graphql-4j](https://github.com/networknt/light-graphql-4j) and [light-hybrid-4j](https://github.com/networknt/light-hybrid-4j) use this library to validate the request and response based on the specifications. If you are using other frameworks like Spring Boot, you can use the [OpenApiValidator](https://github.com/mservicetech/openapi-schema-validation), a generic OpenAPI 3.0 validator based on the OpenAPI 3.0 specification. If you have a project using this library, please submit a PR to add your project below. -* [mpenet/legba](https://github.com/mpenet/legba/) - OpenAPI service library for clojure, adhering to the [RING spec](https://github.com/ring-clojure/ring) +- [mpenet/legba](https://github.com/mpenet/legba/) - OpenAPI service library for clojure, adhering to the [RING spec](https://github.com/ring-clojure/ring) ## Contributors @@ -634,15 +655,12 @@ Thanks to the following people who have contributed to this project. If you are [@nitin1891](https://github.com/nitin1891) - For all contributors, please visit https://github.com/networknt/json-schema-validator/graphs/contributors If you are a contributor, please join the [GitHub Sponsors](https://github.com/sponsors) and switch the link to your sponsors dashboard via a PR. ## Sponsors - ### Individual Sponsors - ### Corporation Sponsors diff --git a/doc/1.x/README.md b/doc/1.x/README.md new file mode 100644 index 000000000..c9c15a268 --- /dev/null +++ b/doc/1.x/README.md @@ -0,0 +1,648 @@ +[Stack Overflow](https://stackoverflow.com/questions/tagged/light-4j) | +[Google Group](https://groups.google.com/forum/#!forum/light-4j) | +[Gitter Chat](https://gitter.im/networknt/json-schema-validator) | +[Subreddit](https://www.reddit.com/r/lightapi/) | +[Youtube](https://www.youtube.com/channel/UCHCRMWJVXw8iB7zKxF55Byw) | +[Documentation](https://doc.networknt.com/library/json-schema-validator/) | +[Contribution Guide](https://doc.networknt.com/contribute/) | + +[![CI](https://github.com/networknt/json-schema-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/networknt/json-schema-validator/actions/workflows/ci.yml) +[![Maven Central](https://img.shields.io/maven-central/v/com.networknt/json-schema-validator.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3Acom.networknt%20a%3Ajson-schema-validator) +[![codecov.io](https://codecov.io/github/networknt/json-schema-validator/coverage.svg?branch=master)](https://codecov.io/github/networknt/json-schema-validator?branch=master) +[![Javadocs](http://www.javadoc.io/badge/com.networknt/json-schema-validator.svg)](https://www.javadoc.io/doc/com.networknt/json-schema-validator) + +This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](https://json-schema.org/specification) specification for JSON schema validation. This implementation supports [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md). + +In addition, [OpenAPI](doc/openapi.md) 3 request/response validation is supported with the use of the appropriate meta-schema. For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design. + +## JSON Schema Specification compatibility + +[![Supported Dialects](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fsupported_versions.json)](https://bowtie.report/#/implementations/java-networknt-json-schema-validator) +[![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft2020-12.json)](https://bowtie.report/#/dialects/draft2020-12) +[![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft2019-09.json)](https://bowtie.report/#/dialects/draft2019-09) +[![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft7.json)](https://bowtie.report/#/dialects/draft7) +[![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft6.json)](https://bowtie.report/#/dialects/draft6) +[![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft4.json)](https://bowtie.report/#/dialects/draft4) + +Information on the compatibility support for each version, including known issues, can be found in the [Compatibility with JSON Schema versions](doc/compatibility.md) document. + +Since [Draft 2019-09](https://json-schema.org/draft/2019-09/json-schema-validation#rfc.section.7) the `format` keyword only generates annotations by default and does not generate assertions. + +This behavior can be overridden to generate assertions by setting the `setFormatAssertionsEnabled` to `true` in `SchemaValidatorsConfig` or `ExecutionConfig`. + +## Upgrading to new versions + +This library can contain breaking changes in `minor` version releases that may require code changes. + +Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. + +The [Releases](https://github.com/networknt/json-schema-validator/releases) page will contain information on the latest versions. + +## Comparing against other implementations + +The [JSON Schema Validation Comparison](https://github.com/creek-service/json-schema-validation-comparison) project from Creek has an informative [Comparison of JVM based Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/) which compares both the functional and performance characteristics of a number of different Java implementations. +* [Functional comparison](https://www.creekservice.org/json-schema-validation-comparison/functional#summary-results-table) +* [Performance comparison](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) + +The [Bowtie](https://github.com/bowtie-json-schema/bowtie) project has a [report](https://bowtie.report/) that compares functional characteristics of different implementations, including non-Java implementations, but does not do any performance benchmarking. + +## Why this library + +#### Performance + +This should be the fastest Java JSON Schema Validator implementation. + +The following is the benchmark results from the [JSON Schema Validator Perftest](https://github.com/networknt/json-schema-validator-perftest) project that uses the [Java Microbenchmark Harness](https://github.com/openjdk/jmh). + +Note that the benchmark results are highly dependent on the input data workloads and schemas used for the validation. + +In this case this workload is using the Draft 4 specification and largely tests the performance of the evaluating the `properties` keyword. You may refer to [Results of performance comparison of JVM based JSON Schema Validation Implementations](https://www.creekservice.org/json-schema-validation-comparison/performance#json-schema-test-suite-benchmark) for benchmark results for more typical workloads + +If performance is an important consideration, the specific sample workloads should be benchmarked, as there are different performance characteristics when certain keywords are used. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators, and annotation collection will adversely affect performance. + +##### NetworkNT 1.4.1 + +``` +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.testValidate thrpt 10 8352.126 ± 61.870 ops/s +NetworkntBenchmark.testValidate:gc.alloc.rate thrpt 10 721.296 ± 5.342 MB/sec +NetworkntBenchmark.testValidate:gc.alloc.rate.norm thrpt 10 90560.013 ± 0.001 B/op +NetworkntBenchmark.testValidate:gc.count thrpt 10 61.000 counts +NetworkntBenchmark.testValidate:gc.time thrpt 10 68.000 ms +``` + +###### Everit 1.14.1 + +``` +Benchmark Mode Cnt Score Error Units +EveritBenchmark.testValidate thrpt 10 3775.453 ± 44.023 ops/s +EveritBenchmark.testValidate:gc.alloc.rate thrpt 10 1667.345 ± 19.437 MB/sec +EveritBenchmark.testValidate:gc.alloc.rate.norm thrpt 10 463104.030 ± 0.003 B/op +EveritBenchmark.testValidate:gc.count thrpt 10 140.000 counts +EveritBenchmark.testValidate:gc.time thrpt 10 158.000 ms +``` + +#### Functionality + +This implementation is tested against the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite). As tests are continually added to the suite, these test results may not be current. + +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| +| NetworkNt | pass: r:4803 (100.0%) o:2372 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:822 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:906 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1220 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1245 (100.0%) o:637 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | + +* Note that this uses the `JoniRegularExpressionFactory` for the `pattern` and `format` `regex` tests. + +#### Jackson Parser + +This library uses [Jackson](https://github.com/FasterXML/jackson) which is a Java JSON parser that is widely used in other projects. If you are already using the Jackson parser in your project, it is natural to choose this library over others for schema validation. + +#### YAML Support + +The library works with JSON and YAML on both schema definitions and input data. + +#### OpenAPI Support + +The OpenAPI 3.0 specification is using JSON schema to validate the request/response, but there are some differences. With a configuration file, you can enable the library to work with OpenAPI 3.0 validation. + +#### Minimal Dependencies + +Following the design principle of the Light Platform, this library has minimal dependencies to ensure there are no dependency conflicts when using it. + +##### Required Dependencies + +The following are the dependencies that will automatically be included when this library is included. + +```xml + + + org.slf4j + slf4j-api + ${version.slf4j} + + + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson} + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${version.jackson} + + + + + com.ethlo.time + itu + ${version.itu} + +``` + +##### Optional Dependencies + +The following are the optional dependencies that may be required for certain options. + +These are not automatically included and setting the relevant option without adding the library will result in a `ClassNotFoundException`. + +```xml + + + + + org.graalvm.js + js + ${version.graaljs} + + + + + + + org.jruby.joni + joni + ${version.joni} + +``` + +##### Excludable Dependencies + +The following are required dependencies that are automatically included, but can be explicitly excluded if they are not required. + +The YAML dependency can be excluded if this is not required. Attempting to process schemas or input that are YAML will result in a `ClassNotFoundException`. + +```xml + + com.networknt + json-schema-validator + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + +``` + +The Ethlo Time dependency can be excluded if accurate validation of the `date-time` format is not required. The `date-time` format will then use `java.time.OffsetDateTime` to determine if the `date-time` is valid . + +```xml + + com.networknt + json-schema-validator + + + com.ethlo.time + itu + + + +``` + +#### Community + +This library is very active with a lot of contributors. New features and bug fixes are handled quickly by the team members. Because it is an essential dependency of the [light-4j](https://github.com/networknt/light-4j) framework in the same GitHub organization, it will be evolved and maintained along with the framework. + +## Prerequisite + +The library supports Java 8 and up. If you want to build from the source code, you need to install JDK 8 locally. To support multiple version of JDK, you can use [SDKMAN](https://www.networknt.com/tool/sdk/) + +## Usage + +### Adding the dependency + +This package is available on Maven central. + +#### Maven: + +```xml + + com.networknt + json-schema-validator + 1.5.9 + +``` + +#### Gradle: + +```java +dependencies { + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.5.9'); +} +``` + +### Validating inputs against a schema + +The following example demonstrates how inputs are validated against a schema. It comprises the following steps. + +* Creating a schema factory with the default schema dialect and how the schemas can be retrieved. + * Configuring mapping the `$id` to a retrieval URI using `schemaMappers`. + * Configuring how the schemas are loaded using the retrieval URI using `schemaLoaders`. + For instance a `Map schemas` containing a mapping of retrieval URI to schema data as a `String` can by configured using `builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas))`. This also accepts a `Function schemaRetrievalFunction`. +* Creating a configuration for controlling validator behavior. +* Loading a schema from a schema location along with the validator configuration. +* Using the schema to validate the data along with setting any execution specific configuration like for instance the locale or whether format assertions are enabled. + +```java +// This creates a schema factory that will use Draft 2020-12 as the default if $schema is not specified +// in the schema data. If $schema is specified in the schema data then that schema dialect will be used +// instead and this version is ignored. +JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> + // This creates a mapping from $id which starts with https://www.example.org/ to the retrieval URI classpath:schema/ + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:schema/")) +); + +SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); +// By default the JDK regular expression implementation which is not ECMA 262 compliant is used +// Note that setting this requires including optional dependencies +// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); +// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); +SchemaValidatorsConfig config = builder.build(); + +// Due to the mapping the schema will be retrieved from the classpath at classpath:schema/example-main.json. +// If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. +JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.example.org/example-main.json"), config); +String input = "{\r\n" + + " \"main\": {\r\n" + + " \"common\": {\r\n" + + " \"field\": \"invalidfield\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + +Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { + // By default since Draft 2019-09 the format keyword only generates annotations and not assertions + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +}); +``` + +### Validating a schema against a meta-schema + +The following example demonstrates how a schema is validated against a meta-schema. + +This is actually the same as validating inputs against a schema except in this case the input is the schema and the schema used is the meta-schema. + +Note that the meta-schemas for Draft 4, Draft 6, Draft 7, Draft 2019-09 and Draft 2020-12 are bundled with the library and these classpath resources will be used by default. + +```java +JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + +SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); +// By default the JDK regular expression implementation which is not ECMA 262 compliant is used +// Note that setting this requires including optional dependencies +// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); +// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); +SchemaValidatorsConfig config = builder.build(); + +// Due to the mapping the meta-schema will be retrieved from the classpath at classpath:draft/2020-12/schema. +JsonSchema schema = jsonSchemaFactory.getSchema(SchemaLocation.of(SchemaId.V202012), config); +String input = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"key\": {\r\n" + + " \"title\" : \"My key\",\r\n" + + " \"type\": \"invalidtype\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; +Set assertions = schema.validate(input, InputFormat.JSON, executionContext -> { + // By default since Draft 2019-09 the format keyword only generates annotations and not assertions + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +}); +``` +### Results and output formats + +#### Results + +The following types of results are generated by the library. + +| Type | Description +|-------------|------------------- +| Assertions | Validation errors generated by a keyword on a particular input data instance. This is generally described in a `ValidationMessage` or in a `OutputUnit`. Note that since Draft 2019-09 the `format` keyword no longer generates assertions by default and instead generates only annotations unless configured otherwise using a configuration option or by using a meta-schema that uses the appropriate vocabulary. +| Annotations | Additional information generated by a keyword for a particular input data instance. This is generally described in a `OutputUnit`. Annotation collection and reporting is turned off by default. Annotations required by keywords such as `unevaluatedProperties` or `unevaluatedItems` are always collected for evaluation purposes and cannot be disabled but will not be reported unless configured to do so. + +The following information is used to describe both types of results. + +| Type | Description +|-------------------|------------------- +| Evaluation Path | This is the set of keys from the root through which evaluation passes to reach the schema for evaluating the instance. This includes `$ref` and `$dynamicRef`. eg. ```/properties/bar/$ref/properties/bar-prop``` +| Schema Location | This is the canonical IRI of the schema plus the JSON pointer fragment to the schema that was used for evaluating the instance. eg. ```https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop``` +| Instance Location | This is the JSON pointer fragment to the instance data that was being evaluated. eg. ```/bar/bar-prop``` + +Assertions contains the following additional information + +| Type | Description +|-------------------|------------------- +| Message | The validation error message. +| Code | The error code. +| Message Key | The message key used for generating the message for localization. +| Arguments | The arguments used for generating the message. +| Type | The keyword that generated the message. +| 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. +| 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 a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(schemaNode)`. +| 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 a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory` and using `JsonNodes.tokenLocationOf(instanceNode)`. +| Error | The error. +| Details | Additional details that can be set by custom keyword validator implementations. This is not used by the library. + +Annotations contains the following additional information + +| Type | Description +|-------------------|------------------- +| Value | The annotation value generated + +##### Line and Column Information + +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. + +This is done by configuring a `JsonNodeReader` that uses the `LocationJsonNodeFactoryFactory`on the `JsonSchemaFactory`. The `JsonLocation` information can then be retrieved using `JsonNodes.tokenLocationOf(jsonNode)`. + +```java +String schemaData = "{\r\n" + + " \"$id\": \"https://schema/myschema\",\r\n" + + " \"properties\": {\r\n" + + " \"startDate\": {\r\n" + + " \"format\": \"date\",\r\n" + + " \"minLength\": 6\r\n" + + " }\r\n" + + " }\r\n" + + "}"; +String inputData = "{\r\n" + + " \"startDate\": \"1\"\r\n" + + "}"; +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.jsonNodeReader(JsonNodeReader.builder().locationAware().build())); +SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); +JsonSchema schema = factory.getSchema(schemaData, InputFormat.JSON, config); +Set messages = schema.validate(inputData, InputFormat.JSON, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); +}); +List list = messages.stream().collect(Collectors.toList()); +ValidationMessage format = list.get(0); +JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(format.getInstanceNode()); +JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(format.getSchemaNode()); +ValidationMessage minLength = list.get(1); +JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getInstanceNode()); +JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenLocationOf(minLength.getSchemaNode()); + +assertEquals("format", format.getType()); +assertEquals("date", format.getSchemaNode().asText()); +assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); +assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr()); +assertEquals("1", format.getInstanceNode().asText()); +assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); +assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr()); +assertEquals("minLength", minLength.getType()); +assertEquals("6", minLength.getSchemaNode().asText()); +assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); +assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr()); +assertEquals("1", minLength.getInstanceNode().asText()); +assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr()); +assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); +assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); +``` + + +#### Output formats + +This library implements the Flag, List and Hierarchical output formats defined in the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/8270653a9f59fadd2df0d789f22d486254505bbe/jsonschema-validation-output-machines.md). + +The List and Hierarchical output formats are particularly helpful for understanding how the system arrived at a particular result. + +| Output Format | Description +|-------------------|------------------- +| Default | Generates the list of assertions. +| Boolean | Returns `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. +| Flag | Returns an `OutputFlag` object with `valid` having `true` if the validation is successful. Note that the fail fast option is turned on by default for this output format. +| List | Returns an `OutputUnit` object with `details` with a list of `OutputUnit` objects with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. +| Hierarchical | Returns an `OutputUnit` object with a hierarchy of `OutputUnit` objects for the evaluation path with the assertions and annotations. Note that annotations are not collected by default and it has to be enabled as it will impact performance. + +The following example shows how to generate the hierarchical output format with annotation collection and reporting turned on and format assertions turned on. + +```java +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); +SchemaValidatorsConfig config = SchemaValidatorsConfig().builder().formatAssertionsEnabled(true).build(); +JsonSchema schema = factory.getSchema(SchemaLocation.of("https://json-schema.org/schemas/example"), config); + +OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionContext.getExecutionConfig().setAnnotationCollectionFilter(keyword -> true); +}); +``` +The following is sample output from the Hierarchical format. + +```json +{ + "valid" : false, + "evaluationPath" : "", + "schemaLocation" : "https://json-schema.org/schemas/example#", + "instanceLocation" : "", + "droppedAnnotations" : { + "properties" : [ "foo", "bar" ], + "title" : "root" + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/0", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/0", + "instanceLocation" : "/foo", + "errors" : { + "required" : "required property 'unspecified-prop' not found" + } + }, { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/1", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1", + "instanceLocation" : "/foo", + "droppedAnnotations" : { + "properties" : [ "foo-prop" ], + "title" : "foo-title", + "additionalProperties" : [ "foo-prop", "other-prop" ] + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/foo/allOf/1/properties/foo-prop", + "schemaLocation" : "https://json-schema.org/schemas/example#/properties/foo/allOf/1/properties/foo-prop", + "instanceLocation" : "/foo/foo-prop", + "errors" : { + "const" : "must be a constant value 1" + }, + "droppedAnnotations" : { + "title" : "foo-prop-title" + } + } ] + }, { + "valid" : false, + "evaluationPath" : "/properties/bar/$ref", + "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar", + "instanceLocation" : "/bar", + "droppedAnnotations" : { + "properties" : [ "bar-prop" ], + "title" : "bar-title" + }, + "details" : [ { + "valid" : false, + "evaluationPath" : "/properties/bar/$ref/properties/bar-prop", + "schemaLocation" : "https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop", + "instanceLocation" : "/bar/bar-prop", + "errors" : { + "minimum" : "must have a minimum value of 10" + }, + "droppedAnnotations" : { + "title" : "bar-prop-title" + } + } ] + } ] +} +``` + +## Configuration + +### Execution Configuration + +| Name | Description | Default Value +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- +| `annotationCollectionEnabled` | Controls whether annotations are collected during processing. Note that collecting annotations will adversely affect performance. | `false` +| `annotationCollectionFilter` | The predicate used to control which keyword to collect and report annotations for. This requires `annotationCollectionEnabled` to be `true`. | `keyword -> false` +| `locale` | The locale to use for generating messages in the `ValidationMessage`. Note that this value is copied from `SchemaValidatorsConfig` for each execution. | `Locale.getDefault()` +| `failFast` | Whether to return failure immediately when an assertion is generated. Note that this value is copied from `SchemaValidatorsConfig` for each execution but is automatically set to `true` for the Boolean and Flag output formats. | `false` +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` +| `debugEnabled` | Controls whether debug logging is enabled for logging the node information when processing. Note that this will generate a lot of logs that will affect performance. | `false` + +### Schema Validators Configuration + +| Name | Description | Default Value +|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- +| `applyDefaultsStrategy` | The strategy for applying defaults when walking when missing or null nodes are encountered. | `ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY` +| `cacheRefs` | Whether the schemas loaded from refs will be cached and reused for subsequent runs. Setting this to `false` will affect performance but may be neccessary to prevent high memory usage for the cache if multiple nested applicators like `anyOf`, `oneOf` and `allOf` are used. | `true` +| `discriminatorKeywordEnabled` | Whether the `discriminator` keyword is handled according to OpenAPI 3. | `false` +| `errorMessageKeyword` | The keyword to use for custom error messages in the schema. If not set this features is disabled. This is typically set to `errorMessage` or `message`. | `null` +| `executionContextCustomizer` | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run. | `null` +| `failFast` | Whether to return failure immediately when an assertion is generated. | `false` +| `formatAssertionsEnabled` | The default is to generate format assertions from Draft 4 to Draft 7 and to only generate annotations from Draft 2019-09. Setting to `true` or `false` will override the default behavior. | `null` +| `javaSemantics` | Whether java semantics is used for the `type` keyword. | `false` +| `locale` | The locale to use for generating messages in the `ValidationMessage`. | `Locale.getDefault()` +| `losslessNarrowing` | Whether lossless narrowing is used for the `type` keyword. | `false` +| `messageSource` | This is used to retrieve the locale specific messages. | `DefaultMessageSource.getInstance()` +| `nullableKeywordEnabled` | Whether the `nullable` keyword is handled according to OpenAPI 3.0. This affects the `enum` and `type` keywords. | `false` +| `pathType` | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_PATH` to use JSON Path. | `PathType.JSON_POINTER` +| `preloadJsonSchema` | Whether the schema will be preloaded before processing any input. This will use memory but the execution of the validation will be faster. | `true` +| `preloadJsonSchemaRefMaxNestingDepth` | The max depth of the evaluation path to preload when preloading refs. | `40` +| `readOnly` | Whether schema is read only. This affects the `readOnly` keyword. | `null` +| `regularExpressionFactory` | The factory to use to create regular expressions for instance `JoniRegularExpressionFactory` or `GraalJSRegularExpressionFactory`. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown. | `JDKRegularExpressionFactory.getInstance()` +| `schemaIdValidator` | This is used to customize how the `$id` values are validated. Note that the default implementation allows non-empty fragments where no base IRI is specified and also allows non-absolute IRI `$id` values in the root schema. | `JsonSchemaIdValidator.DEFAULT` +| `strict` | This is set whether keywords are strict in their validation. What this does depends on the individual validators. | +| `typeLoose` | Whether types are interpreted in a loose manner. If set to true, a single value can be interpreted as a size 1 array. Strings may also be interpreted as number, integer or boolean. | `false` +| `writeOnly` | Whether schema is write only. This affects the `writeOnly` keyword. | `null` + +## Performance Considerations + +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. + +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. + +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 `SchemaValidatorsConfig`. Disabling this will mean the validators from the references need to be re-created for each validation run which will impact performance. + +Collecting annotations will adversely affect validation performance. + +The earlier draft specifications contain less keywords that can potentially impact performance. For instance the use of the `unevaluatedProperties` or `unevaluatedItems` keyword will trigger annotation collection in the related validators, such as the `properties` or `items` validators. + +This does not mean that using a schema with a later draft specification will automatically cause a performance impact. For instance, the `properties` validator will perform checks to determine if annotations need to be collected, and checks if the meta-schema contains the `unevaluatedProperties` keyword and whether the `unevaluatedProperties` keyword exists adjacent the evaluation path. + +## Security Considerations + +The library assumes that the schemas being loaded are trusted. This security model assumes the use case where the schemas are bundled with the application on the classpath. + +| Issue | Description | Mitigation +|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------- +| Schema Loading | The library by default will load schemas from the classpath and over the internet if needed. | A `DisallowSchemaLoader` can be configured to not allow schema retrieval. Alternatively an `AllowSchemaLoader` can be configured to restrict the retrieval IRIs that are allowed. +| Schema Caching | The library by default preloads and caches references when loading schemas. While there is a max nesting depth when preloading schemas it is still possible to construct a schema that has a fan out that consumes a lot of memory from the server. | Set `cacheRefs` option in `SchemaValidatorsConfig` to false. +| Regular Expressions | The library does not validate if a given regular expression is susceptable to denial of service ([ReDoS](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)). | An `AllowRegularExpressionFactory` can be configured to perform validation on the regular expressions that are allowed. +| Validation Errors | The library by default attempts to return all validation errors. The use of applicators such as `allOf` with a large number of schemas may result in a large number of validation errors taking up memory. | Set `failFast` option in `SchemaValidatorsConfig` to immediately return when the first error is encountered. The `OutputFormat.BOOLEAN` or `OutputFormat.FLAG` also can be used. + +## [Quick Start](doc/quickstart.md) + +## [Customizing Schema Retrieval](doc/schema-retrieval.md) + +## [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md) + +## [OpenAPI Specification](doc/openapi.md) + +## [Validators](doc/validators.md) + +## [Configuration](doc/config.md) + +## [Specification Version](doc/specversion.md) + +## [YAML Validation](doc/yaml.md) + +## [Collector Context](doc/collector-context.md) + +## [JSON Schema Walkers and WalkListeners](doc/walkers.md) + +## [Regular Expressions](doc/ecma-262.md) + +## [Custom Error Messages](doc/cust-msg.md) + +## [Multiple Language](doc/multiple-language.md) + +## [MetaSchema Validation](doc/metaschema-validation.md) + +## [Validating RFC 3339 durations](doc/duration.md) + +## Projects + +The [light-rest-4j](https://github.com/networknt/light-rest-4j), [light-graphql-4j](https://github.com/networknt/light-graphql-4j) and [light-hybrid-4j](https://github.com/networknt/light-hybrid-4j) use this library to validate the request and response based on the specifications. If you are using other frameworks like Spring Boot, you can use the [OpenApiValidator](https://github.com/mservicetech/openapi-schema-validation), a generic OpenAPI 3.0 validator based on the OpenAPI 3.0 specification. + +If you have a project using this library, please submit a PR to add your project below. + +* [mpenet/legba](https://github.com/mpenet/legba/) - OpenAPI service library for clojure, adhering to the [RING spec](https://github.com/ring-clojure/ring) + +## Contributors + +Thanks to the following people who have contributed to this project. If you are using this library, please consider to be a sponsor for one of the contributors. + +[@stevehu](https://github.com/sponsors/stevehu) + +[@prashanth-chaitanya](https://github.com/prashanth-chaitanya) + +[@fdutton](https://github.com/fdutton) + +[@valfirst](https://github.com/valfirst) + +[@BalloonWen](https://github.com/BalloonWen) + +[@jiachen1120](https://github.com/jiachen1120) + +[@ddobrin](https://github.com/ddobrin) + +[@eskabetxe](https://github.com/eskabetxe) + +[@ehrmann](https://github.com/ehrmann) + +[@prashanthjos](https://github.com/prashanthjos) + +[@Subhajitdas298](https://github.com/Subhajitdas298) + +[@FWiesner](https://github.com/FWiesner) + +[@rhwood](https://github.com/rhwood) + +[@jawaff](https://github.com/jawaff) + +[@nitin1891](https://github.com/nitin1891) + + +For all contributors, please visit https://github.com/networknt/json-schema-validator/graphs/contributors + +If you are a contributor, please join the [GitHub Sponsors](https://github.com/sponsors) and switch the link to your sponsors dashboard via a PR. + +## Sponsors + + +### Individual Sponsors + + +### Corporation Sponsors diff --git a/doc/collector-context.md b/doc/1.x/collector-context.md similarity index 100% rename from doc/collector-context.md rename to doc/1.x/collector-context.md diff --git a/doc/1.x/compatibility.md b/doc/1.x/compatibility.md new file mode 100644 index 000000000..3292bf4ba --- /dev/null +++ b/doc/1.x/compatibility.md @@ -0,0 +1,181 @@ +## Compatibility with JSON Schema versions + +[![Supported Dialects](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fsupported_versions.json)](https://bowtie.report/#/implementations/java-networknt-json-schema-validator) +[![Draft 2020-12](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft2020-12.json)](https://bowtie.report/#/dialects/draft2020-12) +[![Draft 2019-09](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft2019-09.json)](https://bowtie.report/#/dialects/draft2019-09) +[![Draft 7](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft7.json)](https://bowtie.report/#/dialects/draft7) +[![Draft 6](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft6.json)](https://bowtie.report/#/dialects/draft6) +[![Draft 4](https://img.shields.io/endpoint?url=https%3A%2F%2Fbowtie.report%2Fbadges%2Fjava-com.networknt-json-schema-validator%2Fcompliance%2Fdraft4.json)](https://bowtie.report/#/dialects/draft4) + +The `pattern` and `format` `regex` validator by default uses the JDK regular expression implementation which is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. The library can however be configured to use a ECMA-262 compliant regular expression implementation such as `GraalJS` or `Joni`. + +Annotation processing and reporting are implemented. Note that the collection of annotations will have an adverse performance impact. + +This implements the Flag, List and Hierarchical output formats defined in the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/8270653a9f59fadd2df0d789f22d486254505bbe/jsonschema-validation-output-machines.md). + +The implementation supports the use of custom keywords, formats, vocabularies and meta-schemas. + +### Known Issues + +There are currently no known issues with the required functionality from the specification. + +The following are the tests results after running the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as at 18 Jun 2024 using version 1.4.1. As the test suite is continously updated, this can result in changes in the results subsequently. + +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| +| NetworkNt | pass: r:4803 (100.0%) o:2372 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:822 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:906 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1220 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1245 (100.0%) o:637 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | + +### Legend + +| Symbol | Meaning | +|:------:|:----------------------| +| 🟢 | Fully implemented | +| 🟡 | Partially implemented | +| 🔴 | Not implemented | +| 🚫 | Not defined | + +### Keywords Support + +| Keyword | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | +|:---------------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| +| $anchor | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| $defs | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| $dynamicAnchor | 🚫 | 🚫 | 🚫 | 🚫 | 🟢 | +| $dynamicRef | 🚫 | 🚫 | 🚫 | 🚫 | 🟢 | +| $id | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| $recursiveAnchor | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | +| $recursiveRef | 🚫 | 🚫 | 🚫 | 🟢 | 🚫 | +| $ref | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| $vocabulary | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| additionalItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| additionalProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| allOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| anyOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| const | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| contains | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| contentEncoding | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| contentMediaType | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| contentSchema | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| definitions | 🟢 | 🟢 | 🟢 | 🚫 | 🚫 | +| dependencies | 🟢 | 🟢 | 🟢 | 🚫 | 🚫 | +| dependentRequired | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| dependentSchemas | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| enum | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| exclusiveMaximum (boolean) | 🟢 | 🚫 | 🚫 | 🚫 | 🚫 | +| exclusiveMaximum (numeric) | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| exclusiveMinimum (boolean) | 🟢 | 🚫 | 🚫 | 🚫 | 🚫 | +| exclusiveMinimum (numeric) | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| if-then-else | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| items | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| maxContains | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| minContains | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| maximum | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| maxItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| maxLength | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| maxProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| minimum | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| minItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| minLength | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| minProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| multipleOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| not | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| oneOf | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| pattern | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| patternProperties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| prefixItems | 🚫 | 🚫 | 🚫 | 🚫 | 🟢 | +| properties | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| propertyNames | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| readOnly | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| required | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| type | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| unevaluatedItems | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| unevaluatedProperties | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| uniqueItems | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| writeOnly | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | + +In accordance with the specification, unknown keywords are treated as annotations. This is customizable by configuring a unknown keyword factory on the respective meta-schema. + +#### Content Encoding + +Since Draft 2019-09, the `contentEncoding` keyword does not generate assertions. + +#### Content Media Type + +Since Draft 2019-09, the `contentMediaType` keyword does not generate assertions. + +#### Content Schema + +The `contentSchema` keyword does not generate assertions. + +#### Pattern + +By default the `pattern` keyword uses the JDK regular expression implementation validating regular expressions. + +This is not ECMA-262 compliant and is thus not compliant with the JSON Schema specification. This is however the more likely desired behavior as other logic will most likely be using the default JDK regular expression implementation to perform downstream processing. + +The library can be configured to use a ECMA-262 compliant regular expression validator which is implemented using [GraalJS](https://github.com/oracle/graaljs) or [Joni](https://github.com/jruby/joni). This can be configured by setting `setRegularExpressionFactory` to the respective `GraalJSRegularExpressionFactory` or `JoniRegularExpressionFactory` instances. + +This also requires adding the `org.graalvm.js:js` or `org.jruby.joni:joni` dependency. + +```xml + + + + + org.graalvm.js + js + ${version.graaljs} + + + + + + + org.jruby.joni + joni + ${version.joni} + +``` + +#### Format + +Since Draft 2019-09 the `format` keyword only generates annotations by default and does not generate assertions. + +This can be configured on a schema basis by using a meta schema with the appropriate vocabulary. + +| Version | Vocabulary | Value | +|:----------------------|---------------------------------------------------------------|-------------------| +| Draft 2019-09 | `https://json-schema.org/draft/2019-09/vocab/format` | `true` | +| Draft 2020-12 | `https://json-schema.org/draft/2020-12/vocab/format-assertion`| `true`/`false` | + +This behavior can be overridden to generate assertions by setting the `setFormatAssertionsEnabled` option to `true`. + +| Format | Draft 4 | Draft 6 | Draft 7 | Draft 2019-09 | Draft 2020-12 | +|:----------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| +| date | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| date-time | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| duration | 🚫 | 🚫 | 🚫 | 🟢 | 🟢 | +| email | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| hostname | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| idn-email | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| idn-hostname | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| ipv4 | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| ipv6 | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| iri | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| iri-reference | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| json-pointer | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| relative-json-pointer | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| regex | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| time | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | +| uri | 🟢 | 🟢 | 🟢 | 🟢 | 🟢 | +| uri-reference | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| uri-template | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | +| uuid | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | + +##### Unknown Formats + +When the format assertion vocabularies are used in a meta schema, in accordance to the specification, unknown formats will result in assertions. If the format assertion vocabularies are not used, unknown formats will only result in assertions if the assertions are enabled and if `setStrict("format", true)`. + +##### Footnotes +1. Note that the validation are only optional for some of the keywords/formats. +2. Refer to the corresponding JSON schema for more information on whether the keyword/format is optional or not. \ No newline at end of file diff --git a/doc/config.md b/doc/1.x/config.md similarity index 100% rename from doc/config.md rename to doc/1.x/config.md diff --git a/doc/cust-msg.md b/doc/1.x/cust-msg.md similarity index 100% rename from doc/cust-msg.md rename to doc/1.x/cust-msg.md diff --git a/doc/custom-meta-schema.md b/doc/1.x/custom-meta-schema.md similarity index 100% rename from doc/custom-meta-schema.md rename to doc/1.x/custom-meta-schema.md diff --git a/doc/duration.md b/doc/1.x/duration.md similarity index 100% rename from doc/duration.md rename to doc/1.x/duration.md diff --git a/doc/1.x/ecma-262.md b/doc/1.x/ecma-262.md new file mode 100644 index 000000000..aea236e02 --- /dev/null +++ b/doc/1.x/ecma-262.md @@ -0,0 +1,89 @@ +# Regular Expressions + +For the `pattern` and `format` `regex` validators there are 3 built in options in the library. + +A custom implementation can be made by implementing `com.networknt.schema.regex.RegularExpressionFactory` to return a custom implementation of `com.networknt.schema.regex.RegularExpression`. + +| Regular Expression Factory | Description | +|--------------------------------------------------|----------------------------------------------------| +| `JDKRegularExpressionFactory` | Uses Java's standard `java.util.regex` and calls the `find()` method. Note that `matches()` is not called as that attempts to match the entire string, implicitly adding anchors. This is the default implementation and does not require any additional libraries. | +| `JoniRegularExpressionFactory` | Uses `org.joni.Regex` with `Syntax.ECMAScript`. This requires adding the `org.jruby.joni:joni` dependency which will require about 2MB. | +| `GraalJSRegularExpressionFactory` | Uses GraalJS with `new RegExp(pattern, 'u')`. This requires adding the `org.graalvm.js:js` dependency which will require about 50MB. | + +## Specification + +The use of Regular Expressions is specified in JSON Schema at https://json-schema.org/draft/2020-12/json-schema-core#name-regular-expressions. + +``` +Keywords MAY use regular expressions to express constraints, or constrain the instance value to be a regular expression. These regular expressions SHOULD be valid according to the regular expression dialect described in ECMA-262, section 21.2.1 [ecma262]. + +Regular expressions SHOULD be built with the "u" flag (or equivalent) to provide Unicode support, or processed in such a way which provides Unicode support as defined by ECMA-262. + +Furthermore, given the high disparity in regular expression constructs support, schema authors SHOULD limit themselves to the following regular expression tokens: + +individual Unicode characters, as defined by the JSON specification [RFC8259]; +simple character classes ([abc]), range character classes ([a-z]); +complemented character classes ([^abc], [^a-z]); +simple quantifiers: "+" (one or more), "*" (zero or more), "?" (zero or one), and their lazy versions ("+?", "*?", "??"); +range quantifiers: "{x}" (exactly x occurrences), "{x,y}" (at least x, at most y, occurrences), {x,} (x occurrences or more), and their lazy versions; +the beginning-of-input ("^") and end-of-input ("$") anchors; +simple grouping ("(...)") and alternation ("|"). +Finally, implementations MUST NOT take regular expressions to be anchored, neither at the beginning nor at the end. This means, for instance, the pattern "es" matches "expression". +``` + +## Considerations when selecting implementation + +If strict compliance with the regular expression dialect described in ECMA-262 is required. Then only the `GraalJS` implementation meets that criteria. + +The `Joni` implementation is configured to attempt to match the ECMA-262 regular expression dialect. However this dialect isn't directly maintained by its maintainers as it doesn't come from its upstream `Oniguruma`. The current implementation has known issues matching inputs with newlines and not respecting `^` and `$` anchors. + +The `JDK` implementation is the default and uses `java.util.regex` with the `find()` method. + +As the implementations are used when validating regular expressions, using `format` `regex`, one consideration is how the regular expression is used. For instance if the system that consumes the input is implemented in Javascript then the `GraalJS` implementation will ensure that this regular expression will work. If the system that consumes the input is implemented in Java then the `JDK` implementation may be better. + +## Configuration of implementation + +The following test case shows how to pass a config object to use the `GraalJS` factory. + +```java +public class RegularExpressionTest { + @Test + public void testInvalidRegexValidatorECMA262() throws Exception { + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() + .regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()) + .build(); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + JsonSchema schema = factory.getSchema("{\r\n" + + " \"format\": \"regex\"\r\n" + + "}", config); + Set errors = schema.validate("\"\\\\a\"", InputFormat.JSON, executionContext -> { + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + assertFalse(errors.isEmpty()); + } +} +``` + +## Performance + +The following is the relative performance of the different implementations. + +``` +Benchmark Mode Cnt Score Error Units +RegularExpressionBenchmark.graaljs thrpt 6 362696.226 ± 15811.099 ops/s +RegularExpressionBenchmark.graaljs:gc.alloc.rate thrpt 6 2584.386 ± 112.708 MB/sec +RegularExpressionBenchmark.graaljs:gc.alloc.rate.norm thrpt 6 7472.003 ± 0.001 B/op +RegularExpressionBenchmark.graaljs:gc.count thrpt 6 130.000 counts +RegularExpressionBenchmark.graaljs:gc.time thrpt 6 144.000 ms +RegularExpressionBenchmark.jdk thrpt 6 2776184.321 ± 41838.479 ops/s +RegularExpressionBenchmark.jdk:gc.alloc.rate thrpt 6 1482.565 ± 22.343 MB/sec +RegularExpressionBenchmark.jdk:gc.alloc.rate.norm thrpt 6 560.000 ± 0.001 B/op +RegularExpressionBenchmark.jdk:gc.count thrpt 6 74.000 counts +RegularExpressionBenchmark.jdk:gc.time thrpt 6 78.000 ms +RegularExpressionBenchmark.joni thrpt 6 1810229.581 ± 35230.798 ops/s +RegularExpressionBenchmark.joni:gc.alloc.rate thrpt 6 1463.887 ± 28.483 MB/sec +RegularExpressionBenchmark.joni:gc.alloc.rate.norm thrpt 6 848.003 ± 0.001 B/op +RegularExpressionBenchmark.joni:gc.count thrpt 6 73.000 counts +RegularExpressionBenchmark.joni:gc.time thrpt 6 77.000 ms +``` + diff --git a/doc/metaschema-validation.md b/doc/1.x/metaschema-validation.md similarity index 100% rename from doc/metaschema-validation.md rename to doc/1.x/metaschema-validation.md diff --git a/doc/1.x/multiple-language.md b/doc/1.x/multiple-language.md new file mode 100644 index 000000000..92881501d --- /dev/null +++ b/doc/1.x/multiple-language.md @@ -0,0 +1,70 @@ +The error messages have been translated to several languages by contributors, defined in the `jsv-messages.properties` resource +bundle under https://github.com/networknt/json-schema-validator/tree/master/src/main/resources. To use one of the +available translations the simplest approach is to set your default locale before running the validation: + +```java +// Set the default locale to German (needs only to be set once before using the validator) +Locale.setDefault(Locale.GERMAN); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source); +... +``` + +Note that the above approach changes the locale for the entire JVM which is probably not what you want to do if you are +using this in an application expected to support multiple languages (for example a localised web application). In this +case you should use the `SchemaValidatorsConfig` class before loading your schema: + +```java +// Set the configuration with a specific locale (you can create this before each validation) +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setLocale(myLocale); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); +... +``` + +Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you +choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that your resource bundle covers all the keys defined by the default bundle. + +```java +// Set the configuration with a custom message source +MessageSource messageSource = new ResourceBundleMessageSource("my-messages"); +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setMessageSource(messageSource); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); +... +``` + +It is possible to override specific keys from the default resource bundle. Note however that you will need to supply all the languages for that specific key as it will not fallback on the default resource bundle. For instance the jsv-messages-override resource bundle will take precedence when resolving the message key. + +```java +// Set the configuration with a custom message source +MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME); +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setMessageSource(messageSource); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); +... +``` + +The following approach can be used to determine the locale to use on a per user basis using a language tag priority list. + +```java +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); +JsonSchema schema = factory.getSchema(source, config); + +// Uses the fr locale for this user +Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); +ExecutionContext executionContext = jsonSchema.createExecutionContext(); +executionContext.getExecutionConfig().setLocale(locale); +Set messages = jsonSchema.validate(executionContext, rootNode); + +// Uses the it locale for this user +locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); +executionContext = jsonSchema.createExecutionContext(); +executionContext.getExecutionConfig().setLocale(locale); +messages = jsonSchema.validate(executionContext, rootNode); +... +``` diff --git a/doc/openapi-discriminators.md b/doc/1.x/openapi-discriminators.md similarity index 100% rename from doc/openapi-discriminators.md rename to doc/1.x/openapi-discriminators.md diff --git a/doc/1.x/openapi.md b/doc/1.x/openapi.md new file mode 100644 index 000000000..da1c6ab45 --- /dev/null +++ b/doc/1.x/openapi.md @@ -0,0 +1,48 @@ +# OpenAPI Specification + +The library includes support for the [OpenAPI Specification](https://swagger.io/specification/). + +## Validating a request / response defined in an OpenAPI document + +The library can be used to validate requests and responses with the use of the appropriate meta-schema. + +| Dialect | Meta-schema | +|--------------------------------------------------|----------------------------------------------------| +| `https://spec.openapis.org/oas/3.0/dialect` | `com.networknt.schema.oas.OpenApi30.getInstance()` | +| `https://spec.openapis.org/oas/3.1/dialect/base` | `com.networknt.schema.oas.OpenApi31.getInstance()` | + +```java +JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.metaSchema(OpenApi31.getInstance()) + .defaultMetaSchemaIri(OpenApi31.getInstance().getIri())); +JsonSchema schema = factory.getSchema(SchemaLocation.of( + "classpath:schema/oas/3.1/petstore.yaml#/components/schemas/PetRequest")); +String input = "{\r\n" + + " \"petType\": \"dog\",\r\n" + + " \"bark\": \"woof\"\r\n" + + "}"; +Set messages = schema.validate(input, InputFormat.JSON); +``` + +## Validating an OpenAPI document + +The library can be used to validate OpenAPI documents, however the OpenAPI meta-schema documents are not bundled with the library. + +It is recommended that the relevant meta-schema documents are placed in the classpath and are mapped otherwise they will be loaded over the internet. + +The following are the documents required to validate a OpenAPI 3.1 document +* `https://spec.openapis.org/oas/3.1/schema-base/2022-10-07` +* `https://spec.openapis.org/oas/3.1/schema/2022-10-07` +* `https://spec.openapis.org/oas/3.1/dialect/base` +* `https://spec.openapis.org/oas/3.1/meta/base` + +```java +SchemaValidatorsConfig config = new SchemaValidatorsConfig(); +config.setPathType(PathType.JSON_POINTER); +JsonSchema schema = JsonSchemaFactory + .getInstance(VersionFlag.V202012, + builder -> builder.schemaMappers(schemaMappers -> schemaMappers + .mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1"))) + .getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config); +Set messages = schema.validate(openApiDocument, InputFormat.JSON); +``` diff --git a/doc/1.x/quickstart.md b/doc/1.x/quickstart.md new file mode 100644 index 000000000..f427e7b7e --- /dev/null +++ b/doc/1.x/quickstart.md @@ -0,0 +1,142 @@ +## Quick Start + +To use the validator, the `JsonSchema` first needs to be loaded. For performance it is recommended that the `JsonSchema` is cached. + +The following examples demonstrate loading the `JsonSchema` in the following manner. +* `SchemaLocation` with the value of the `$id` of the schema which is mapped using the `SchemaMapper` to the retrieval IRI which is on the classpath +* `SchemaLocation` with the value of the `$id` of the schema where the content of the schema is supplied using the `SchemaLoader` +* `SchemaLocation` with the value of the retrieval IRI which is on the classpath +* `String` with the content of the schema +* `JsonNode` with the content of the schema + +The preferred method of loading a schema is by using a `SchemaLocation` and by configuring the appropriate `SchemaMapper` and `SchemaLoader` on the `JsonSchemaFactory`. The `SchemaMapper` is use to map the `$id` to the retrieval IRI. The `SchemaLoader` is used to actually load the content of the schema. + +Loading a schema from a `String` or `JsonNode` is not recommended as a relative `$ref` will not be properly resolved as there is no base IRI. + +```java +package com.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collections; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * Sample test. + */ +public class SampleTest { + @Test + void schemaFromSchemaLocationMapping() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.schemaMappers( + schemaMappers -> schemaMappers.mapPrefix("https://www.example.com/schema", "classpath:schema"))); + /* + * This should be cached for performance. + */ + JsonSchema schemaFromSchemaLocation = factory + .getSchema(SchemaLocation.of("https://www.example.com/schema/example-ref.json")); + /* + * By default all schemas are preloaded eagerly but ref resolve failures are not + * thrown. You check if there are issues with ref resolving using + * initializeValidators() + */ + schemaFromSchemaLocation.initializeValidators(); + Set errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertEquals(1, errors.size()); + } + + @Test + void schemaFromSchemaLocationContent() throws JsonMappingException, JsonProcessingException { + String schemaData = "{\"enum\":[1, 2, 3, 4]}"; + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas( + Collections.singletonMap("https://www.example.com/schema/example-ref.json", schemaData)))); + /* + * This should be cached for performance. + */ + JsonSchema schemaFromSchemaLocation = factory + .getSchema(SchemaLocation.of("https://www.example.com/schema/example-ref.json")); + /* + * By default all schemas are preloaded eagerly but ref resolve failures are not + * thrown. You check if there are issues with ref resolving using + * initializeValidators() + */ + schemaFromSchemaLocation.initializeValidators(); + Set errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertEquals(1, errors.size()); + } + + @Test + void schemaFromClasspath() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + /* + * This should be cached for performance. + * + * Loading from using the retrieval IRI is not recommended as it may cause + * confusing when resolving relative $ref when $id is also used. + */ + JsonSchema schemaFromClasspath = factory.getSchema(SchemaLocation.of("classpath:schema/example-ref.json")); + /* + * By default all schemas are preloaded eagerly but ref resolve failures are not + * thrown. You check if there are issues with ref resolving using + * initializeValidators() + */ + schemaFromClasspath.initializeValidators(); + Set errors = schemaFromClasspath.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertEquals(1, errors.size()); + } + + @Test + void schemaFromString() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + /* + * This should be cached for performance. + * + * Loading from a String is not recommended as there is no base IRI to use for + * resolving relative $ref. + */ + JsonSchema schemaFromString = factory + .getSchema("{\"enum\":[1, 2, 3, 4]}"); + Set errors = schemaFromString.validate("7", InputFormat.JSON, + executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertEquals(1, errors.size()); + } + + @Test + void schemaFromJsonNode() throws JsonMappingException, JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + JsonNode schemaNode = JsonMapperFactory.getInstance().readTree( + "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}"); + /* + * This should be cached for performance. + * + * Loading from a JsonNode is not recommended as there is no base IRI to use for + * resolving relative $ref. + * + * Note that the V202012 from the factory is the default version if $schema is not + * specified. As $schema is specified in the data, V6 is used. + */ + JsonSchema schemaFromNode = factory.getSchema(schemaNode); + /* + * By default all schemas are preloaded eagerly but ref resolve failures are not + * thrown. You check if there are issues with ref resolving using + * initializeValidators() + */ + schemaFromNode.initializeValidators(); + Set errors = schemaFromNode.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertEquals(1, errors.size()); + } +} +``` diff --git a/doc/1.x/schema-retrieval.md b/doc/1.x/schema-retrieval.md new file mode 100644 index 000000000..43842e50a --- /dev/null +++ b/doc/1.x/schema-retrieval.md @@ -0,0 +1,158 @@ +# Customizing Schema Retrieval + +A schema can be identified by its schema identifier which is indicated using the `$id` keyword or `id` keyword in earlier drafts. This is an absolute IRI that uniquely identifies the schema and is not necessarily a network locator. A schema need not be downloadable from it's absolute IRI. + +In the event a schema references a schema identifier that is not a subschema resource, for instance defined in the `$defs` keyword or `definitions` keyword. The library will need to be able to retrieve the schema given its schema identifier. + +In the event that the schema does not define a schema identifier using the `$id` keyword, the retrieval IRI will be used as it's schema identifier. + +## Loading Schemas from memory + +Schemas can be loaded through a map. + +```java +String schemaData = "{\r\n" + + " \"type\": \"integer\"\r\n" + + "}"; +Map schemas = Collections.singletonMap("https://www.example.com/integer.json", schemaData); +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas))); +``` + +Schemas can be loaded through a function. + +```java +String schemaData = "{\r\n" + + " \"type\": \"integer\"\r\n" + + "}"; +Map schemas = Collections.singletonMap("https://www.example.com/integer.json", schemaData); + JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas::get))); +``` + +Schemas can also be loaded in the following manner. + +```java +class RegistryEntry { + private final String schemaData; + + public RegistryEntry(String schemaData) { + this.schemaData = schemaData; + } + + public String getSchemaData() { + return this.schemaData; + } +} + +String schemaData = "{\r\n" + + " \"type\": \"integer\"\r\n" + + "}"; +Map registry = Collections + .singletonMap("https://www.example.com/integer.json", new RegistryEntry(schemaData)); +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, builder -> builder + .schemaLoaders(schemaLoaders -> schemaLoaders.schemas(registry::get, RegistryEntry::getSchemaData))); +``` + +## Mapping Schema Identifier to Retrieval IRI + +The schema identifier can be mapped to the retrieval IRI by implementing the `SchemaMapper` interface. + +### Configuring Schema Mapper + +```java +class CustomSchemaMapper implements SchemaMapper { + @Override + public AbsoluteIri map(AbsoluteIri absoluteIRI) { + String iri = absoluteIRI.toString(); + if ("https://www.example.com/integer.json".equals(iri)) { + return AbsoluteIri.of("classpath:schemas/integer.json"); + } + return null; + } +} + +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder.schemaMappers(schemaMappers -> schemaMappers.add(new CustomSchemaMapper()))); +``` + +### Configuring Prefix Mappings + +```java +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder + .schemaMappers(schemaMappers -> schemaMappers + .mapPrefix("https://json-schema.org", "classpath:") + .mapPrefix("http://json-schema.org", "classpath:"))); +``` + +### Configuring Mappings + +```java +Map mappings = Collections + .singletonMap("https://www.example.com/integer.json", "classpath:schemas/integer.json"); + +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder.schemaMappers(schemaMappers -> schemaMappers.mappings(mappings))); +``` + +## Customizing Network Schema Retrieval + +The default `UriSchemaLoader` implementation uses JDK connection/socket without handling network exceptions. It works in most of the cases; however, if you want to have a customized implementation, you can do so. One user has his implementation with urirest to handle the timeout. A detailed discussion can be found in this [issue](https://github.com/networknt/json-schema-validator/issues/240) + +### Configuring Custom URI Schema Loader + +The default `UriSchemaLoader` can be overwritten in order to customize its behaviour in regards of authorization or error handling. + +The `SchemaLoader` interface must implemented and the implementation configured on the `JsonSchemaFactory`. + +```java +public class CustomUriSchemaLoader implements SchemaLoader { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomUriSchemaLoader.class); + private final String authorizationToken; + private final HttpClient client; + + public CustomUriSchemaLoader(String authorizationToken) { + this.authorizationToken = authorizationToken; + this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); + } + + @Override + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + String scheme = absoluteIri.getScheme(); + if ("https".equals(scheme) || "http".equals(scheme)) { + URI uri = URI.create(absoluteIri.toString()); + return () -> { + HttpRequest request = HttpRequest.newBuilder().uri(uri).header("Authorization", authorizationToken).build(); + try { + HttpResponse response = this.client.send(request, HttpResponse.BodyHandlers.ofString()); + if ((200 > response.statusCode()) || (response.statusCode() > 299)) { + String errorMessage = String.format("Could not get data from schema endpoint. The following status %d was returned.", response.statusCode()); + LOGGER.error(errorMessage); + } + return new ByteArrayInputStream(response.body().getBytes(StandardCharsets.UTF_8)); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + return null; + } +} +``` + +Within the `JsonSchemaFactory` the custom `SchemaLoader` must be configured. + +```java +CustomUriSchemaLoader uriSchemaLoader = new CustomUriSchemaLoader(authorizationToken); + +JsonSchemaFactory schemaFactory = JsonSchemaFactory + .getInstance(VersionFlag.V7, + builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.add(uriSchemaLoader))); +``` diff --git a/doc/specversion.md b/doc/1.x/specversion.md similarity index 100% rename from doc/specversion.md rename to doc/1.x/specversion.md diff --git a/doc/validators.md b/doc/1.x/validators.md similarity index 100% rename from doc/validators.md rename to doc/1.x/validators.md diff --git a/doc/walk_flow.png b/doc/1.x/walk_flow.png similarity index 100% rename from doc/walk_flow.png rename to doc/1.x/walk_flow.png diff --git a/doc/1.x/walkers.md b/doc/1.x/walkers.md new file mode 100644 index 000000000..8cbc6c2f6 --- /dev/null +++ b/doc/1.x/walkers.md @@ -0,0 +1,291 @@ +### JSON Schema Walkers + +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, or applying default values. JSON walkers were introduced to complement the validation functionality this library already provides. + +Currently, walking is defined at the validator instance level for all the built-in keywords. + +### Walk methods + +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. + +```java +public interface JsonSchemaWalker { + /** + * + * This method gives the capability to walk through the given JsonNode, allowing + * functionality beyond validation like collecting information,handling cross + * cutting concerns like logging or instrumentation. This method also performs + * the validation if {@code shouldValidateSchema} is set to true.
+ *
+ * {@link BaseJsonValidator#walk(ExecutionContext, JsonNode, JsonNode, JsonNodePath, boolean)} provides + * a default implementation of this method. However validators that parse + * sub-schemas should override this method to call walk method on those + * sub-schemas. + * + * @param executionContext ExecutionContext + * @param node JsonNode + * @param rootNode JsonNode + * @param instanceLocation JsonNodePath + * @param shouldValidateSchema boolean + * @return a set of validation messages if shouldValidateSchema is true. + */ + Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema); +} + +``` + +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. + +```java + /** + * This is default implementation of walk method. Its job is to call the + * validate method if shouldValidateSchema is enabled. + */ + @Override + default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) + : Collections.emptySet(); + } +``` + +A new walk method added to the JSONSchema class allows us to walk through the JSONSchema. + +```java + public ValidationResult walk(JsonNode node, boolean validate) { + return walk(createExecutionContext(), node, validate); + } + + @Override + public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + JsonNodePath instanceLocation, boolean shouldValidateSchema) { + Set errors = new LinkedHashSet<>(); + // Walk through all the JSONWalker's. + for (JsonValidator validator : getValidators()) { + JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath(); + try { + // Call all the pre-walk listeners. If at least one of the pre walk listeners + // returns SKIP, then skip the walk. + if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + this, validator)) { + Set results = null; + try { + results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + } finally { + if (results != null && !results.isEmpty()) { + errors.addAll(results); + } + } + } + } finally { + // Call all the post-walk listeners. + this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, + evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, + this, validator, errors); + } + } + return errors; + } +``` +Following code snippet shows how to call the walk method on a JsonSchema instance. + +```java +ValidationResult result = jsonSchema.walk(data, false); + +``` + +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. + +### Walk Listeners + +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. + +```java +public interface JsonSchemaWalkListener { + + public WalkFlow onWalkStart(WalkEvent walkEvent); + + public void onWalkEnd(WalkEvent walkEvent, Set validationMessages); +} +``` + +Following is the example of a sample WalkListener implementation. + +```java +private static class PropertiesKeywordListener implements JsonSchemaWalkListener { + + @Override + public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { + JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); + if (schemaNode.get("title").textValue().equals("Property3")) { + return WalkFlow.SKIP; + } + return WalkFlow.CONTINUE; + } + + @Override + public void onWalkEnd(WalkEvent keywordWalkEvent, Set validationMessages) { + + } + } +``` +If the onWalkStart method returns WalkFlow.SKIP, the actual walk method execution will be skipped. + +Walk listeners can be added by using the SchemaValidatorsConfig class. + +```java +SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); + schemaValidatorsConfig.keywordWalkListener(new AllKeywordListener()); + schemaValidatorsConfig.keywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener()); + schemaValidatorsConfig.keywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), + new PropertiesKeywordListener()); +final JsonSchemaFactory schemaFactory = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) + .build(); +this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig.build()); + +``` + +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. + +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. + +```java +SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); +schemaValidatorsConfig.propertyWalkListener(new ExamplePropertyWalkListener()); +final JsonSchemaFactory schemaFactory = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) + .build(); +this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig.build()); + +``` + +### Walk Events + +An instance of WalkEvent is passed to both the onWalkStart and onWalkEnd methods of the WalkListeners implementations. + +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. + +Following snippet shows the details captured by WalkEvent instance. + +```java +public class WalkEvent { + private ExecutionContext executionContext; + private JsonSchema schema; + private String keyword; + private JsonNode rootNode; + private JsonNode instanceNode; + private JsonNodePath instanceLocation; + private JsonValidator validator; + ... +} +``` + +### Sample Flow + +Given an example schema as shown, if we write a property listener, the walk flow is as depicted in the image. + +```json +{ + + "title": "Sample Schema", + "definitions" : { + "address" :{ + "street-address": { + "title": "Street Address", + "type": "string" + }, + "pincode": { + "title": "Body", + "type": "integer" + } + } + }, + "properties": { + "name": { + "title": "Title", + "type": "string", + "maxLength": 50 + }, + "body": { + "title": "Body", + "type": "string" + }, + "address": { + "title": "Excerpt", + "$ref": "#/definitions/address" + } + + }, + "additionalProperties": false +} +``` + +![img](walk_flow.png) + + +Few important points to note about the flow. + +1. onWalkStart and onWalkEnd are the methods defined in the property walk listener +2. Anywhere during the flow, onWalkStart can return a WalkFlow.SKIP to stop the walk method execution of a particular "property schema". +3. onWalkEnd will be called even if the onWalkStart returns a WalkFlow.SKIP. +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. + 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. +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, + Our property listener would be invoked for each of the property defined in the "$ref" schema. +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. + Since the walk method will not be called any property or keyword listeners in the "property schema" will not be invoked. + + +### Applying defaults + +In some use cases we may want to apply defaults while walking the schema. +To accomplish this, create an ApplyDefaultsStrategy when creating a SchemaValidatorsConfig. +The input object is changed in place, even if validation fails, or a fail-fast or some other exception is thrown. + +Here is the order of operations in walker. +1. apply defaults +1. run listeners +1. validate if shouldValidateSchema is true + +Suppose the JSON schema is +```json +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Schema with default values ", + "type": "object", + "properties": { + "intValue": { + "type": "integer", + "default": 15, + "minimum": 20 + } + }, + "required": ["intValue"] +} +``` + +A JSON file like +```json +{ +} +``` + +would normally fail validation as "intValue" is required. +But if we apply defaults while walking, then required validation passes, and the object is changed in place. + +```java + JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); + SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); + schemaValidatorsConfig.applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)); + JsonSchema jsonSchema = schemaFactory.getSchema(SchemaLocation.of("classpath:schema.json"), schemaValidatorsConfig.build()); + + JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data.json")); + ValidationResult result = jsonSchema.walk(inputNode, true); + assertThat(result.getValidationMessages(), Matchers.empty()); + assertEquals("{\"intValue\":15}", inputNode.toString()); + assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()), + Matchers.containsInAnyOrder("$.intValue: must have a minimum value of 20.")); +``` diff --git a/doc/yaml-line-numbers.md b/doc/1.x/yaml-line-numbers.md similarity index 100% rename from doc/yaml-line-numbers.md rename to doc/1.x/yaml-line-numbers.md diff --git a/doc/yaml.md b/doc/1.x/yaml.md similarity index 100% rename from doc/yaml.md rename to doc/1.x/yaml.md diff --git a/doc/compatibility.md b/doc/compatibility.md index 3292bf4ba..ba5507c0a 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -19,11 +19,11 @@ The implementation supports the use of custom keywords, formats, vocabularies an There are currently no known issues with the required functionality from the specification. -The following are the tests results after running the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as at 18 Jun 2024 using version 1.4.1. As the test suite is continously updated, this can result in changes in the results subsequently. +The following are the tests results after running the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) as at 13 Oct 2025 using version 2.0.0. As the test suite is continously updated, this can result in changes in the results subsequently. -| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | -|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| -| NetworkNt | pass: r:4803 (100.0%) o:2372 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:822 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:906 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1220 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1245 (100.0%) o:637 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +| --------------- | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------- | +| NetworkNt | pass: r:4840 (100.0%) o:2421 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | | pass: r:610 (100.0%) o:255 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:829 (100.0%) o:322 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:913 (100.0%) o:554 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1227 (100.0%) o:639 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1261 (100.0%) o:651 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | ### Legend @@ -174,7 +174,7 @@ This behavior can be overridden to generate assertions by setting the `setFormat ##### Unknown Formats -When the format assertion vocabularies are used in a meta schema, in accordance to the specification, unknown formats will result in assertions. If the format assertion vocabularies are not used, unknown formats will only result in assertions if the assertions are enabled and if `setStrict("format", true)`. +When the format assertion vocabularies are used in a meta schema, in accordance to the specification, unknown formats will result in assertions. If the format assertion vocabularies are not used, unknown formats will only result in assertions if the assertions are enabled and if `strict("format", true)` for `SchemaRegistryConfig`. ##### Footnotes 1. Note that the validation are only optional for some of the keywords/formats. diff --git a/doc/custom-dialect.md b/doc/custom-dialect.md new file mode 100644 index 000000000..fd90a19a2 --- /dev/null +++ b/doc/custom-dialect.md @@ -0,0 +1,196 @@ +# Customizing Dialects, Vocabularies, Keywords and Formats + +The dialects, vocabularies, keywords and formats can be customized with appropriate configuration of the `SchemaRegistry` that is used to create instances of `Schema`. + +## Creating a custom keyword + +A custom keyword can be implemented by implementing the `com.networknt.schema.keyword.Keyword` interface. + +```java +public class EqualsKeyword implements Keyword { + @Override + public String getValue() { + return "equals"; + } + @Override + public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, + JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) + throws JsonSchemaException, Exception { + return new EqualsValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, this, validationContext, false); + } +} +``` + +```java +public class EqualsValidator extends BaseKeywordValidator { + private final String value; + + EqualsValidator(SchemaLocation schemaLocation, JsonNode schemaNode, + Schema parentSchema, Keyword keyword, + SchemaContext schemaContext) { + super(keyword, schemaNode, schemaLocation, parentSchema, schemaContext); + this.value = schemaNode.textValue(); + } + + @Override + public void validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, + NodePath instanceLocation) { + if (!node.asText().equals(value)) { + executionContext.addError(error().message("must be equal to ''{0}''") + .arguments(value) + .instanceLocation(instanceLocation).instanceNode(node).evaluationPath(executionContext.getEvaluationPath()).build()); + } + } +} +``` + +## Adding a keyword to a standard dialect + +A custom keyword can be added to a standard dialect by customizing its dialect which is identified by its id. + +The following adds a custom keyword to the Draft 2020-12 dialect. + +```java +Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .keyword(new EqualsKeyword()) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +## Creating a custom dialect + +A custom dialect can be created by using a standard dialect as a base. + +The following creates a custom dialect `https://www.example.com/schema` with a custom keyword using the Draft 2020-12 dialect as a base. + +```java +Dialect dialect = Dialect.builder("https://www.example.com/schema", Dialects.getDraft202012()) + .keyword(new EqualsKeyword()) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +## Associating vocabularies to a dialect + +Custom vocabularies can be associated with a particular dialect by configuring a `com.networknt.schema.vocabulary.VocabularyRegistry` on its dialect. + +```java +VocabularyRegistry vocabularyRegistry = id -> { + if ("https://www.example.com/vocab/equals".equals(id)) { + return new Vocabulary("https://www.example.com/vocab/equals", new EqualsKeyword()); + } + return null; +}; +Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .vocabularyRegistry(vocabularyRegistry) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +The following custom meta-schema for the dialect `https://www.example.com/schema` will use the custom vocabulary `https://www.example.com/vocab/equals`. + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.example.com/schema", + "$vocabulary": { + "https://www.example.com/vocab/equals": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/core": true + }, + "allOf": [ + { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" }, + { "$ref": "https://json-schema.org/draft/2020-12/meta/core" } + ] +} +``` + +Note that `"https://www.example.com/vocab/equals": true` means that if the vocabulary is unknown the meta-schema for the dialect will fail to successfully load while `"https://www.example.com/vocab/equals": false` means that an unknown vocabulary will still successfully load. + +## Unknown keywords + +By default unknown keywords are treated as annotations. This can be customized by configuring a `com.networknt.schema.keyword.KeywordFactory` on its dialect. + +The following configuration will cause a `InvalidSchemaException` to be thrown if an unknown keyword is used. + +```java +Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .unknownKeywordFactory(DisallowUnknownKeywordFactory.getInstance()) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +## Creating a custom format + +A custom format can be implemented by implementing the `com.networknt.schema.format.Format` interface. + +```java +public class MatchNumberFormat implements Format { + private final BigDecimal compare; + + public MatchNumberFormat(BigDecimal compare) { + this.compare = compare; + } + @Override + public boolean matches(ExecutionContext executionContext, ValidationContext validationContext, JsonNode value) { + JsonType nodeType = TypeFactory.getValueNodeType(value, validationContext.getConfig()); + if (nodeType != JsonType.NUMBER && nodeType != JsonType.INTEGER) { + return true; + } + BigDecimal number = value.isBigDecimal() ? value.decimalValue() : BigDecimal.valueOf(value.doubleValue()); + number = new BigDecimal(number.toPlainString()); + return number.compareTo(compare) == 0; + } + @Override + public String getName() { + return "matchnumber"; + } +} +``` + +## Adding a format to a standard dialect + +A custom format can be added to a standard dialect by customizing its dialect which is identified by its id. + +The following adds a custom format to the Draft 2020-12 dialect. + +```java +Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .format(new MatchNumberFormat(new BigDecimal("12345"))) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +## Customizing the format keyword + +The format keyword implementation to use can be customized by supplying a `FormatKeywordFactory` to the dialect that creates an instance of the subclass of `FormatKeyword`. + +```java +Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .formatKeywordFactory(CustomFormatKeyword::new) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); +``` + +## Unknown formats + +By default unknown formats are ignored unless the format assertion vocabulary is used for that dialect. Note that the format annotation vocabulary with the configuration to enable format assertions is not equivalent to the format assertion vocabulary. + +To ensure that errors are raised when unknown formats are used, the `SchemaRegistryConfig` can be configured to set `format` as strict. + + +## Loading dialects + +Creating the `SchemaRegistry` with `withDefaultDialect` will create one that accepts all other standard dialects such as Draft 7 or Draft 2019-09 as the meta-schemas for those dialects will be automatically loaded. This will also attempt to load custom meta-schemas with custom vocabularies. + +```java +SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); +``` + +As Draft 2020-12 was specified, it will be used by default if `$schema` is not defined. + +If this is undesirable, creating the `SchemaRegistry` with `withDialect` will create one that only accepts that particular dialect. + +```java +SchemaRegistry factory = SchemaRegistry.withDialect(Dialects.getDraft202012()); +``` diff --git a/doc/ecma-262.md b/doc/ecma-262.md index aea236e02..5ddc28572 100644 --- a/doc/ecma-262.md +++ b/doc/ecma-262.md @@ -49,15 +49,15 @@ The following test case shows how to pass a config object to use the `GraalJS` f public class RegularExpressionTest { @Test public void testInvalidRegexValidatorECMA262() throws Exception { - SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() - .regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()) - .build(); - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); - JsonSchema schema = factory.getSchema("{\r\n" + SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()).build(); + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); + Schema schema = schemaRegistry.getSchema("{\r\n" + " \"format\": \"regex\"\r\n" - + "}", config); - Set errors = schema.validate("\"\\\\a\"", InputFormat.JSON, executionContext -> { - executionContext.getExecutionConfig().setFormatAssertionsEnabled(true); + + "}"); + List errors = schema.validate("\"\\\\a\"", InputFormat.JSON, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); }); assertFalse(errors.isEmpty()); } diff --git a/doc/error-message.md b/doc/error-message.md new file mode 100644 index 000000000..5dd63aced --- /dev/null +++ b/doc/error-message.md @@ -0,0 +1,111 @@ +# Custom Error Messages +Schema authors can provide their own custom messages within the schema using a specified keyword. + +This is not enabled by default and the `SchemaRegistryConfig` must be configured with the `errorMessageKeyword`. + +```java +SchemaRegistryConfig config = SchemaRegistryConfig.builder().errorMessageKeyword("errorMessage").build(); +``` + +## Examples +### Example 1 : +The custom message can be provided outside properties for each type, as shown in the schema below. +```json +{ + "type": "object", + "properties": { + "firstName": { + "type": "string", + "description": "The person's first name." + }, + "foo": { + "type": "array", + "maxItems": 3 + } + }, + "errorMessage": { + "maxItems": "MaxItem must be 3 only", + "type": "Invalid type" + } +} +``` +### Example 2 : +To keep custom messages distinct for each type, one can even give them in each property. +```json +{ + "type": "object", + "properties": { + "dateTime": { + "type": "string", + "format": "date", + "errorMessage": { + "format": "Keep date format yyyy-mm-dd" + } + }, + "uuid": { + "type": "string", + "format": "uuid", + "errorMessage": { + "format": "Input should be uuid" + } + } + } +} +``` +### Example 3 : +For the keywords `required` and `dependencies`, different messages can be specified for different properties. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "errorMessage": { + "type": "should be an object", + "required": { + "foo": "'foo' is required", + "bar": "'bar' is required" + } + } +} +``` +### Example 4 : +The message can use arguments but note that single quotes need to be escaped as `java.text.MessageFormat` will be used to format the message. + +```json +{ + "type": "object", + "properties": { + "foo": { + "type": "number" + }, + "bar": { + "type": "string" + } + }, + "required": ["foo", "bar"], + "errorMessage": { + "type": "should be an object", + "required": { + "foo": "{0}: ''foo'' is required", + "bar": "{0}: ''bar'' is required" + } + } +} +``` + +## Format +```json +"errorMessage": { + "[keyword]": "[customMessage]" +} +``` +Users can provide custom messages in the configured keyword, typically `errorMessage` or `message` field. +The `keyword` should be the key and the `customMessage` should be the value. \ No newline at end of file diff --git a/doc/migration-2.0.0.md b/doc/migration-2.0.0.md index 8294847f9..24422a056 100644 --- a/doc/migration-2.0.0.md +++ b/doc/migration-2.0.0.md @@ -9,9 +9,12 @@ ### Major Changes +- Configuration on a per Schema basis is no longer possible. - Removal of deprecated methods and functionality from 1.x. - Major renaming of many of the public APIs and moving of classes into sub-packages. - Errors are returned as a `List` instead of a `Set`. +- Error messages do not have the `instanceLocation` as part of the message. +- Error codes have been removed. - External resources will not be automatically fetched by default. This now requires opt-in via configuration. - This is to conform to the specification that requires such functionality to be disabled by default to prefer offline operation. Note however that classpath resources will still be automatically loaded. @@ -51,32 +54,32 @@ The `com.networknt.schema.SchemaValidatorsConfig` file has been replaced by either `com.networknt.schema.SchemaRegistryConfig` or `com.networknt.schema.walk.WalkConfig` or moved to `com.networknt.schema.ExecutionConfig` and can no longer be configured on a per schema basis. -| Name | Migration | -| ------------------------------------- | -------------------------------------------------------- | -| `applyDefaultsStrategy` | `com.networknt.schema.walk.WalkConfig` | -| `cacheRefs` | `com.networknt.schema.SchemaRegistryConfig` | -| `discriminatorKeywordEnabled` | Removed. Dialect must contain a `discriminator` keyword. | -| `errorMessageKeyword` | `com.networknt.schema.SchemaRegistryConfig` | -| `executionContextCustomizer` | `com.networknt.schema.SchemaRegistryConfig` | -| `failFast` | `com.networknt.schema.SchemaRegistryConfig` | -| `formatAssertionsEnabled` | `com.networknt.schema.SchemaRegistryConfig` | -| `javaSemantics` | `com.networknt.schema.SchemaRegistryConfig` | -| `locale` | `com.networknt.schema.SchemaRegistryConfig` | -| `losslessNarrowing` | `com.networknt.schema.SchemaRegistryConfig` | -| `messageSource` | `com.networknt.schema.SchemaRegistryConfig` | -| `nullableKeywordEnabled` | Removed. Dialect must contain a `nullable` keyword. | -| `pathType` | `com.networknt.schema.SchemaRegistryConfig` | -| `preloadJsonSchema` | `com.networknt.schema.SchemaRegistryConfig` | -| `preloadJsonSchemaRefMaxNestingDepth` | `com.networknt.schema.SchemaRegistryConfig` | -| `readOnly` | `com.networknt.schema.ExecutionConfig` | -| `regularExpressionFactory` | `com.networknt.schema.SchemaRegistryConfig` | -| `schemaIdValidator` | `com.networknt.schema.SchemaRegistryConfig` | -| `strict` | `com.networknt.schema.SchemaRegistryConfig` | -| `typeLoose` | `com.networknt.schema.SchemaRegistryConfig` | -| `writeOnly` | `com.networknt.schema.ExecutionConfig` | -| `itemWalkListeners` | `com.networknt.schema.walk.WalkConfig` | -| `keywordWalkListeners` | `com.networknt.schema.walk.WalkConfig` | -| `propertyWalkListeners` | `com.networknt.schema.walk.WalkConfig` | +| Name | Migration | +| ------------------------------------- | --------------------------------------------------------------------------------------- | +| `applyDefaultsStrategy` | `com.networknt.schema.walk.WalkConfig` | +| `cacheRefs` | `com.networknt.schema.SchemaRegistryConfig` | +| `discriminatorKeywordEnabled` | Removed. Dialect must contain a `discriminator` keyword. | +| `errorMessageKeyword` | `com.networknt.schema.SchemaRegistryConfig` | +| `executionContextCustomizer` | `com.networknt.schema.SchemaRegistryConfig` | +| `failFast` | `com.networknt.schema.SchemaRegistryConfig` | +| `formatAssertionsEnabled` | `com.networknt.schema.SchemaRegistryConfig` | +| `javaSemantics` | Removed as this did the same thing as `losslessNarrowing`. | +| `locale` | `com.networknt.schema.SchemaRegistryConfig` | +| `losslessNarrowing` | `com.networknt.schema.SchemaRegistryConfig` | +| `messageSource` | `com.networknt.schema.SchemaRegistryConfig` | +| `nullableKeywordEnabled` | Removed. Dialect must contain a `nullable` keyword. | +| `pathType` | `com.networknt.schema.SchemaRegistryConfig` | +| `preloadJsonSchema` | `com.networknt.schema.SchemaRegistryConfig` | +| `preloadJsonSchemaRefMaxNestingDepth` | Removed. No longer needed as evaluation context is no longer stored as validator state. | +| `readOnly` | `com.networknt.schema.ExecutionConfig` | +| `regularExpressionFactory` | `com.networknt.schema.SchemaRegistryConfig` | +| `schemaIdValidator` | `com.networknt.schema.SchemaRegistryConfig` | +| `strict` | `com.networknt.schema.SchemaRegistryConfig` | +| `typeLoose` | `com.networknt.schema.SchemaRegistryConfig` | +| `writeOnly` | `com.networknt.schema.ExecutionConfig` | +| `itemWalkListeners` | `com.networknt.schema.walk.WalkConfig` | +| `keywordWalkListeners` | `com.networknt.schema.walk.WalkConfig` | +| `propertyWalkListeners` | `com.networknt.schema.walk.WalkConfig` | #### API @@ -136,3 +139,85 @@ public class Demo { } } ``` + +#### Configuration Examples + +##### Support all standard dialects but defaults to Draft 2020-12 when not specified using $schema + +```java +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); +``` + +##### Only supports Draft 2020-12 and defaults to it when not specified using $schema + +```java +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +``` + +##### Only supports OpenAPI 3.1 and defaults to it when not specified using $schema + +```java +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getOpenApi31()); +``` + +##### Provide schema resource using string + +```java +Map schemas = new HashMap<>(); + schemas.put("https://example.com/address.schema.json", schemaData); + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemas(schemas)); +``` + +##### Map schema resource id to classpath + +```java +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + .mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1"))); +``` + +##### Fetch remote schema resources + +```java +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaLoader(schemaLoader -> schemaLoader.fetchRemoteResources())); +``` + +##### Force the format keyword always behave as an assertion even after Draft 2019-09 + +```java +SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder().formatAssertionsEnabled(true) + .build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); +``` + +##### Use [joni](https://github.com/jruby/joni) regular expression implementation instead of JDK which has better ECMA compliance + +```java +SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(JoniRegularExpressionFactory.getInstance()).build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); +``` + +##### Get schema using schema location + +```java +Schema schema = schemaRegistry.getSchema(SchemaLocation.of("https://example.com/address.schema.json")); +``` + +##### Get schema using schema location + +```java +Schema schema = schemaRegistry.getSchema(SchemaLocation.of("https://example.com/address.schema.json")); +``` + +##### Get schema using a string with the schema data + +**_NOTE_**: The schema may not have a base `$id` to properly resolve references to other schema documents. + +```java +Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON); +``` diff --git a/doc/multiple-language.md b/doc/multiple-language.md index 92881501d..ba14fed1a 100644 --- a/doc/multiple-language.md +++ b/doc/multiple-language.md @@ -1,38 +1,67 @@ +# Locale + The error messages have been translated to several languages by contributors, defined in the `jsv-messages.properties` resource -bundle under https://github.com/networknt/json-schema-validator/tree/master/src/main/resources. To use one of the -available translations the simplest approach is to set your default locale before running the validation: +bundle under https://github.com/networknt/json-schema-validator/tree/master/src/main/resources. + +## Global Configuration + +To use one of the available translations the simplest approach is to set your default locale before running the validation: ```java // Set the default locale to German (needs only to be set once before using the validator) Locale.setDefault(Locale.GERMAN); -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); -JsonSchema schema = factory.getSchema(source); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +JsonSchema schema = schemaRegistry.getSchema(schemaData); ... ``` Note that the above approach changes the locale for the entire JVM which is probably not what you want to do if you are using this in an application expected to support multiple languages (for example a localised web application). In this -case you should use the `SchemaValidatorsConfig` class before loading your schema: +case you should use the `SchemRegistryConfig` class before loading your schema: + +## Per-Request Configuration + +```java +// Set the configuration with a specific locale for each request +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +Schema schema = schemaRegistry.getSchema(schemaData); +List errors = schema.validate(input, InputFormat.JSON, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig.locale(Locale.GERMAN)); +}); +... +``` + +The following approach can be used to determine the locale to use on a per user basis using a language tag priority list. ```java -// Set the configuration with a specific locale (you can create this before each validation) -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setLocale(myLocale); -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); -JsonSchema schema = factory.getSchema(source, config); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); +Schema schema = schemaRegistry.getSchema(schemaData); + +// Uses the fr locale for this user +List errors = jsonSchema.validate(input, executionContext -> { + Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); // fr + executionContext.executionConfig(executionConfig -> executionConfig.locale(locale)); +}); + +// Uses the it locale for this user +errors = jsonSchema.validate(input, executionContext -> { + Locale locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); // it + executionContext.executionConfig(executionConfig -> executionConfig.locale(locale)); +}); ... ``` +## Message Source + Besides setting the locale and using the default resource bundle, you may also specify your own to cover any languages you choose without adapting the library's source, or to override default messages. In doing so you however you should ensure that your resource bundle covers all the keys defined by the default bundle. ```java // Set the configuration with a custom message source MessageSource messageSource = new ResourceBundleMessageSource("my-messages"); -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setMessageSource(messageSource); -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); -JsonSchema schema = factory.getSchema(source, config); +SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder().messageSource(messageSource).build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); +JsonSchema schema = schemaRegistry.getSchema(schemaData); ... ``` @@ -40,31 +69,12 @@ It is possible to override specific keys from the default resource bundle. Note ```java // Set the configuration with a custom message source + MessageSource messageSource = new ResourceBundleMessageSource("jsv-messages-override", DefaultMessageSource.BUNDLE_BASE_NAME); -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setMessageSource(messageSource); -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); -JsonSchema schema = factory.getSchema(source, config); +SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder().messageSource(messageSource).build(); +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); +JsonSchema schema = schemaRegistry.getSchema(schemaData); ... ``` -The following approach can be used to determine the locale to use on a per user basis using a language tag priority list. - -```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909); -JsonSchema schema = factory.getSchema(source, config); - -// Uses the fr locale for this user -Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); -ExecutionContext executionContext = jsonSchema.createExecutionContext(); -executionContext.getExecutionConfig().setLocale(locale); -Set messages = jsonSchema.validate(executionContext, rootNode); -// Uses the it locale for this user -locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); -executionContext = jsonSchema.createExecutionContext(); -executionContext.getExecutionConfig().setLocale(locale); -messages = jsonSchema.validate(executionContext, rootNode); -... -``` diff --git a/doc/openapi.md b/doc/openapi.md index da1c6ab45..2bcf199c2 100644 --- a/doc/openapi.md +++ b/doc/openapi.md @@ -4,24 +4,22 @@ The library includes support for the [OpenAPI Specification](https://swagger.io/ ## Validating a request / response defined in an OpenAPI document -The library can be used to validate requests and responses with the use of the appropriate meta-schema. +The library can be used to validate requests and responses with the use of the appropriate dialect. -| Dialect | Meta-schema | -|--------------------------------------------------|----------------------------------------------------| -| `https://spec.openapis.org/oas/3.0/dialect` | `com.networknt.schema.oas.OpenApi30.getInstance()` | -| `https://spec.openapis.org/oas/3.1/dialect/base` | `com.networknt.schema.oas.OpenApi31.getInstance()` | +| Dialect | Instance | +| ------------------------------------------------ | ------------------------------------------------------ | +| `https://spec.openapis.org/oas/3.0/dialect` | `com.networknt.schema.dialect.Dialects.getOpenApi30()` | +| `https://spec.openapis.org/oas/3.1/dialect/base` | `com.networknt.schema.dialect.Dialects.getOpenApi31()` | ```java -JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, - builder -> builder.metaSchema(OpenApi31.getInstance()) - .defaultMetaSchemaIri(OpenApi31.getInstance().getIri())); -JsonSchema schema = factory.getSchema(SchemaLocation.of( +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getOpenApi31()); +Schema schema = schemaRegistry.getSchema(SchemaLocation.of( "classpath:schema/oas/3.1/petstore.yaml#/components/schemas/PetRequest")); String input = "{\r\n" + " \"petType\": \"dog\",\r\n" + " \"bark\": \"woof\"\r\n" + "}"; -Set messages = schema.validate(input, InputFormat.JSON); +List errors = schema.validate(input, InputFormat.JSON); ``` ## Validating an OpenAPI document @@ -31,18 +29,17 @@ The library can be used to validate OpenAPI documents, however the OpenAPI meta- It is recommended that the relevant meta-schema documents are placed in the classpath and are mapped otherwise they will be loaded over the internet. The following are the documents required to validate a OpenAPI 3.1 document -* `https://spec.openapis.org/oas/3.1/schema-base/2022-10-07` -* `https://spec.openapis.org/oas/3.1/schema/2022-10-07` -* `https://spec.openapis.org/oas/3.1/dialect/base` -* `https://spec.openapis.org/oas/3.1/meta/base` + +- `https://spec.openapis.org/oas/3.1/schema-base/2022-10-07` +- `https://spec.openapis.org/oas/3.1/schema/2022-10-07` +- `https://spec.openapis.org/oas/3.1/dialect/base` +- `https://spec.openapis.org/oas/3.1/meta/base` ```java -SchemaValidatorsConfig config = new SchemaValidatorsConfig(); -config.setPathType(PathType.JSON_POINTER); -JsonSchema schema = JsonSchemaFactory - .getInstance(VersionFlag.V202012, - builder -> builder.schemaMappers(schemaMappers -> schemaMappers +Schema schema = SchemaRegistry + .withDefaultDialect(SpecificationVersion.DRAFT_2020_12, + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers .mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1"))) - .getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config); -Set messages = schema.validate(openApiDocument, InputFormat.JSON); + .getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07")); +List errors = schema.validate(openApiDocument, InputFormat.JSON); ``` diff --git a/doc/quickstart.md b/doc/quickstart.md index f427e7b7e..6269f6756 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -1,15 +1,15 @@ ## Quick Start -To use the validator, the `JsonSchema` first needs to be loaded. For performance it is recommended that the `JsonSchema` is cached. +To use the validator, the `Schema` first needs to be loaded. For performance it is recommended that the `Schema` is cached. -The following examples demonstrate loading the `JsonSchema` in the following manner. -* `SchemaLocation` with the value of the `$id` of the schema which is mapped using the `SchemaMapper` to the retrieval IRI which is on the classpath +The following examples demonstrate loading the `Schema` in the following manner. +* `SchemaLocation` with the value of the `$id` of the schema which is mapped using the `SchemaIdResolvers` to the retrieval IRI which is on the classpath * `SchemaLocation` with the value of the `$id` of the schema where the content of the schema is supplied using the `SchemaLoader` * `SchemaLocation` with the value of the retrieval IRI which is on the classpath * `String` with the content of the schema * `JsonNode` with the content of the schema -The preferred method of loading a schema is by using a `SchemaLocation` and by configuring the appropriate `SchemaMapper` and `SchemaLoader` on the `JsonSchemaFactory`. The `SchemaMapper` is use to map the `$id` to the retrieval IRI. The `SchemaLoader` is used to actually load the content of the schema. +The preferred method of loading a schema is by using a `SchemaLocation` and by configuring the appropriate `SchemaIdResolver` and `SchemaLoader` on the `SchemaRegistry`. The `SchemaMapper` is use to map the `$id` to the retrieval IRI. The `SchemaLoader` is used to actually load the content of the schema. Loading a schema from a `String` or `JsonNode` is not recommended as a relative `$ref` will not be properly resolved as there is no base IRI. @@ -19,14 +19,12 @@ package com.example; import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Collections; -import java.util.Set; +import java.util.List; import org.junit.jupiter.api.Test; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.serialization.JsonMapperFactory; /** @@ -34,13 +32,14 @@ import com.networknt.schema.serialization.JsonMapperFactory; */ public class SampleTest { @Test - void schemaFromSchemaLocationMapping() throws JsonMappingException, JsonProcessingException { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.schemaMappers( - schemaMappers -> schemaMappers.mapPrefix("https://www.example.com/schema", "classpath:schema"))); + void schemaFromSchemaLocationMapping() { + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12, + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + .mapPrefix("https://www.example.com/schema", "classpath:schema"))); /* * This should be cached for performance. */ - JsonSchema schemaFromSchemaLocation = factory + Schema schemaFromSchemaLocation = schemaRegistry .getSchema(SchemaLocation.of("https://www.example.com/schema/example-ref.json")); /* * By default all schemas are preloaded eagerly but ref resolve failures are not @@ -48,22 +47,23 @@ public class SampleTest { * initializeValidators() */ schemaFromSchemaLocation.initializeValidators(); - Set errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, - executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + List errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext + .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } @Test - void schemaFromSchemaLocationContent() throws JsonMappingException, JsonProcessingException { + void schemaFromSchemaLocationContent() { String schemaData = "{\"enum\":[1, 2, 3, 4]}"; - - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, - builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas( - Collections.singletonMap("https://www.example.com/schema/example-ref.json", schemaData)))); + + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12, + builder -> builder.schemas( + Collections.singletonMap("https://www.example.com/schema/example-ref.json", schemaData))); /* * This should be cached for performance. */ - JsonSchema schemaFromSchemaLocation = factory + Schema schemaFromSchemaLocation = schemaRegistry .getSchema(SchemaLocation.of("https://www.example.com/schema/example-ref.json")); /* * By default all schemas are preloaded eagerly but ref resolve failures are not @@ -71,51 +71,52 @@ public class SampleTest { * initializeValidators() */ schemaFromSchemaLocation.initializeValidators(); - Set errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, - executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + List errors = schemaFromSchemaLocation.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext + .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } @Test - void schemaFromClasspath() throws JsonMappingException, JsonProcessingException { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + void schemaFromClasspath() { + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); /* * This should be cached for performance. * * Loading from using the retrieval IRI is not recommended as it may cause * confusing when resolving relative $ref when $id is also used. */ - JsonSchema schemaFromClasspath = factory.getSchema(SchemaLocation.of("classpath:schema/example-ref.json")); + Schema schemaFromClasspath = schemaRegistry.getSchema(SchemaLocation.of("classpath:schema/example-ref.json")); /* * By default all schemas are preloaded eagerly but ref resolve failures are not * thrown. You check if there are issues with ref resolving using * initializeValidators() */ schemaFromClasspath.initializeValidators(); - Set errors = schemaFromClasspath.validate("{\"id\": \"2\"}", InputFormat.JSON, - executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + List errors = schemaFromClasspath.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext + .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } @Test - void schemaFromString() throws JsonMappingException, JsonProcessingException { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + void schemaFromString() { + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); /* * This should be cached for performance. * * Loading from a String is not recommended as there is no base IRI to use for * resolving relative $ref. */ - JsonSchema schemaFromString = factory - .getSchema("{\"enum\":[1, 2, 3, 4]}"); - Set errors = schemaFromString.validate("7", InputFormat.JSON, - executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + Schema schemaFromString = schemaRegistry.getSchema("{\"enum\":[1, 2, 3, 4]}"); + List errors = schemaFromString.validate("7", InputFormat.JSON, executionContext -> executionContext + .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } @Test - void schemaFromJsonNode() throws JsonMappingException, JsonProcessingException { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + void schemaFromJsonNode() throws JsonProcessingException { + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); JsonNode schemaNode = JsonMapperFactory.getInstance().readTree( "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}"); /* @@ -124,18 +125,19 @@ public class SampleTest { * Loading from a JsonNode is not recommended as there is no base IRI to use for * resolving relative $ref. * - * Note that the V202012 from the factory is the default version if $schema is not - * specified. As $schema is specified in the data, V6 is used. + * Note that the V202012 from the schemaRegistry is the default version if $schema is + * not specified. As $schema is specified in the data, V6 is used. */ - JsonSchema schemaFromNode = factory.getSchema(schemaNode); + Schema schemaFromNode = schemaRegistry.getSchema(schemaNode); /* * By default all schemas are preloaded eagerly but ref resolve failures are not * thrown. You check if there are issues with ref resolving using * initializeValidators() */ schemaFromNode.initializeValidators(); - Set errors = schemaFromNode.validate("{\"id\": \"2\"}", InputFormat.JSON, - executionContext -> executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + List errors = schemaFromNode.validate("{\"id\": \"2\"}", InputFormat.JSON, + executionContext -> executionContext + .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } } diff --git a/doc/schema-retrieval.md b/doc/schema-retrieval.md index 43842e50a..921969f1a 100644 --- a/doc/schema-retrieval.md +++ b/doc/schema-retrieval.md @@ -15,9 +15,8 @@ String schemaData = "{\r\n" + " \"type\": \"integer\"\r\n" + "}"; Map schemas = Collections.singletonMap("https://www.example.com/integer.json", schemaData); -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas))); +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.schemas(schemas)); ``` Schemas can be loaded through a function. @@ -27,9 +26,8 @@ String schemaData = "{\r\n" + " \"type\": \"integer\"\r\n" + "}"; Map schemas = Collections.singletonMap("https://www.example.com/integer.json", schemaData); - JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.schemas(schemas::get))); +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.schemas(schemas::get)); ``` Schemas can also be loaded in the following manner. @@ -52,9 +50,8 @@ String schemaData = "{\r\n" + "}"; Map registry = Collections .singletonMap("https://www.example.com/integer.json", new RegistryEntry(schemaData)); -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, builder -> builder - .schemaLoaders(schemaLoaders -> schemaLoaders.schemas(registry::get, RegistryEntry::getSchemaData))); +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.schemas(registry::get, RegistryEntry::getSchemaData)); ``` ## Mapping Schema Identifier to Retrieval IRI @@ -64,9 +61,9 @@ The schema identifier can be mapped to the retrieval IRI by implementing the `Sc ### Configuring Schema Mapper ```java -class CustomSchemaMapper implements SchemaMapper { +class CustomSchemaIdResolver implements SchemaIdResolver { @Override - public AbsoluteIri map(AbsoluteIri absoluteIRI) { + public AbsoluteIri resolve(AbsoluteIri absoluteIRI) { String iri = absoluteIRI.toString(); if ("https://www.example.com/integer.json".equals(iri)) { return AbsoluteIri.of("classpath:schemas/integer.json"); @@ -75,20 +72,19 @@ class CustomSchemaMapper implements SchemaMapper { } } -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder.schemaMappers(schemaMappers -> schemaMappers.add(new CustomSchemaMapper()))); +SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder + .schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers.add(new CustomSchemaIdResolver()))) ``` ### Configuring Prefix Mappings ```java -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder - .schemaMappers(schemaMappers -> schemaMappers - .mapPrefix("https://json-schema.org", "classpath:") - .mapPrefix("http://json-schema.org", "classpath:"))); +SchemaRegistry schemaRegistry = SchemaRegistry + .withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + .mapPrefix("https://json-schema.org", "classpath:") + .mapPrefix("http://json-schema.org", "classpath:"))); ``` ### Configuring Mappings @@ -97,9 +93,9 @@ JsonSchemaFactory schemaFactory = JsonSchemaFactory Map mappings = Collections .singletonMap("https://www.example.com/integer.json", "classpath:schemas/integer.json"); -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder.schemaMappers(schemaMappers -> schemaMappers.mappings(mappings))); +SchemaRegistry schemaRegistry = SchemaRegistry + .withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers.mappings(mappings))); ``` ## Customizing Network Schema Retrieval @@ -108,17 +104,17 @@ The default `UriSchemaLoader` implementation uses JDK connection/socket without ### Configuring Custom URI Schema Loader -The default `UriSchemaLoader` can be overwritten in order to customize its behaviour in regards of authorization or error handling. +The default `IriResourceLoader` can be overwritten in order to customize its behaviour in regards of authorization or error handling. -The `SchemaLoader` interface must implemented and the implementation configured on the `JsonSchemaFactory`. +The `ResourceLoader` interface must implemented and the implementation configured on the `SchemaRegistry`. ```java -public class CustomUriSchemaLoader implements SchemaLoader { - private static final Logger LOGGER = LoggerFactory.getLogger(CustomUriSchemaLoader.class); +public class CustomUriResourceLoader implements ResourceLoader { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomUriResourceLoader.class); private final String authorizationToken; private final HttpClient client; - public CustomUriSchemaLoader(String authorizationToken) { + public CustomUriResourceLoader(String authorizationToken) { this.authorizationToken = authorizationToken; this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); } @@ -147,12 +143,12 @@ public class CustomUriSchemaLoader implements SchemaLoader { } ``` -Within the `JsonSchemaFactory` the custom `SchemaLoader` must be configured. +Within the `SchemaRegistry` the custom `ResourceLoader` must be configured. ```java -CustomUriSchemaLoader uriSchemaLoader = new CustomUriSchemaLoader(authorizationToken); +CustomUriResourceLoader uriResourceLoader = new CustomUriResourceLoader(authorizationToken); -JsonSchemaFactory schemaFactory = JsonSchemaFactory - .getInstance(VersionFlag.V7, - builder -> builder.schemaLoaders(schemaLoaders -> schemaLoaders.add(uriSchemaLoader))); +SchemaRegistry schemaRegistry = SchemaRegistry + .withDefaultDialect(SpecificationVersion.DRAFT_7, + builder -> builder.resourceLoaders(resourceLoaders -> resourceLoaders.add(uriSchemaLoader))); ``` diff --git a/doc/upgrading.md b/doc/upgrading.md index 2577cee32..e9a3e7842 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -4,6 +4,29 @@ This library can contain breaking changes in `minor` version releases. This contains information on the notable or breaking changes in each version. +### 2.0.0 + +| Compatibility | Version | +| ------------- | --------- | +| Java | Java 8 | +| Jackson | Jackson 2 | + +#### Major Changes + +- Configuration on a per Schema basis is no longer possible. +- Removal of deprecated methods and functionality from 1.x. +- Major renaming of many of the public APIs and moving of classes into sub-packages. +- Errors are returned as a `List` instead of a `Set`. +- Error messages do not have the `instanceLocation` as part of the message. +- Error codes have been removed. +- External resources will not be automatically fetched by default. This now requires opt-in via configuration. + - This is to conform to the specification that requires such functionality to be disabled by default to prefer offline operation. Note however that classpath resources will still be automatically loaded. + +#### Migration + +The migration document can be found [here](migration-2.0.0.md). + + ### 1.4.1 #### Schema Validators Config @@ -23,24 +46,26 @@ SchemaValidatorsConfig config = SchemaValidatorsConfig.builder() ``` The following configurations were renamed with the old ones deprecated -* `handleNullableField` -> `nullableKeywordEnabled` -* `openAPI3StyleDiscriminators` -> `discriminatorKeywordEnabled` -* `customMessageSupported` -> `errorMessageKeyword` + +- `handleNullableField` -> `nullableKeywordEnabled` +- `openAPI3StyleDiscriminators` -> `discriminatorKeywordEnabled` +- `customMessageSupported` -> `errorMessageKeyword` The following defaults were changed in the builder vs the constructor -* `pathType` from `PathType.LEGACY` to `PathType.JSON_POINTER` -* `handleNullableField` from `true` to `false` -* `customMessageSupported` from `true` to `false` + +- `pathType` from `PathType.LEGACY` to `PathType.JSON_POINTER` +- `handleNullableField` from `true` to `false` +- `customMessageSupported` from `true` to `false` When using the builder custom error messages are not enabled by default and must be enabled by specifying the error message keyword to use ie. "message". -| Deprecated Code | Replacement -|------------------------------------------------------------------------|---------------------------------------------------------------------- -| `SchemaValidatorsConfig config = new SchemaValidatorsConfig();` | `SchemaValidatorsConfig config = SchemaValidatorsConfig().builder().pathType(PathType.LEGACY).errorMessageKeyword("message").nullableKeywordEnabled(true).build();` -| `config.setEcma262Validator(true);` | `builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance());` -| `config.setHandleNullableField(true);` | `builder.nullableKeywordEnabled(true);` -| `config.setOpenAPI3StyleDiscriminators(true);` | `builder.discriminatorKeywordEnabled(true);` -| `config.setCustomMessageSupported(true);` | `builder.errorMessageKeyword("message");` +| Deprecated Code | Replacement | +| --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SchemaValidatorsConfig config = new SchemaValidatorsConfig();` | `SchemaValidatorsConfig config = SchemaValidatorsConfig().builder().pathType(PathType.LEGACY).errorMessageKeyword("message").nullableKeywordEnabled(true).build();` | +| `config.setEcma262Validator(true);` | `builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance());` | +| `config.setHandleNullableField(true);` | `builder.nullableKeywordEnabled(true);` | +| `config.setOpenAPI3StyleDiscriminators(true);` | `builder.discriminatorKeywordEnabled(true);` | +| `config.setCustomMessageSupported(true);` | `builder.errorMessageKeyword("message");` | #### Collector Context @@ -88,10 +113,10 @@ The preferred way of configuring the implementation is via setting the `regularE Previously the if debug logging is enabled the validators will log fine grained logs. This now requires setting the `debugEnabled` flag in `ExecutionConfig` as the checks to determine if the logger was enabled was impacting performance. - ### 1.4.0 -This contains breaking changes +This contains breaking changes + - to those using the walk functionality - in how custom meta-schemas are created @@ -102,65 +127,68 @@ The behavior for the property listener is now more consistent whether or not val The following are the breaking changes to those using the walk functionality. `WalkEvent` -| Field | Change | Notes +| Field | Change | Notes |--------------------------|--------------|---------- -| `schemaLocation` | Removed | For keywords: `getValidator().getSchemaLocation()`. For items and properties: `getSchema().getSchemaLocation()` -| `evaluationPath` | Removed | For keywords: `getValidator().getEvaluationPath()`. For items and properties: `getSchema().getEvaluationPath()` -| `schemaNode` | Removed | `getSchema().getSchemaNode()` -| `parentSchema` | Removed | `getSchema().getParentSchema()` -| `schema` | New | For keywords this is the parent schema of the validator. For items and properties this is the item or property schema being evaluated. -| `node` | Renamed | `instanceNode` -| `currentJsonSchemaFactory`| Removed | `getSchema().getValidationContext().getJsonSchemaFactory()` -| `validator` | New | The validator indicated by the keyword. - +| `schemaLocation` | Removed | For keywords: `getValidator().getSchemaLocation()`. For items and properties: `getSchema().getSchemaLocation()` +| `evaluationPath` | Removed | For keywords: `getValidator().getEvaluationPath()`. For items and properties: `getSchema().getEvaluationPath()` +| `schemaNode` | Removed | `getSchema().getSchemaNode()` +| `parentSchema` | Removed | `getSchema().getParentSchema()` +| `schema` | New | For keywords this is the parent schema of the validator. For items and properties this is the item or property schema being evaluated. +| `node` | Renamed | `instanceNode` +| `currentJsonSchemaFactory`| Removed | `getSchema().getValidationContext().getJsonSchemaFactory()` +| `validator` | New | The validator indicated by the keyword. The following are the breaking changes in how custom meta-schemas are created. `JsonSchemaFactory` -* The following were renamed on `JsonSchemaFactory` builder - * `defaultMetaSchemaURI` -> `defaultMetaSchemaIri` - * `enableUriSchemaCache` -> `enableSchemaCache` -* The builder now accepts a `JsonMetaSchemaFactory` which can be used to restrict the loading of meta-schemas that aren't explicitly defined in the `JsonSchemaFactory`. The `DisallowUnknownJsonMetaSchemaFactory` can be used to only allow explicitly configured meta-schemas. + +- The following were renamed on `JsonSchemaFactory` builder + - `defaultMetaSchemaURI` -> `defaultMetaSchemaIri` + - `enableUriSchemaCache` -> `enableSchemaCache` +- The builder now accepts a `JsonMetaSchemaFactory` which can be used to restrict the loading of meta-schemas that aren't explicitly defined in the `JsonSchemaFactory`. The `DisallowUnknownJsonMetaSchemaFactory` can be used to only allow explicitly configured meta-schemas. `JsonMetaSchema` -* In particular `Version201909` and `Version202012` had most of the keywords moved to their respective vocabularies. -* The following were renamed - * `getUri` -> `getIri` -* The builder now accepts a `vocabularyFactory` to allow for custom vocabularies. -* The builder now accepts a `unknownKeywordFactory`. By default this uses the `UnknownKeywordFactory` implementation that logs a warning and returns a `AnnotationKeyword`. The `DisallowUnknownKeywordFactory` can be used to disallow the use of unknown keywords. -* The implementation of the builder now correctly throws an exception for `$vocabulary` with value of `true` that are not known to the implementation. + +- In particular `Version201909` and `Version202012` had most of the keywords moved to their respective vocabularies. +- The following were renamed + - `getUri` -> `getIri` +- The builder now accepts a `vocabularyFactory` to allow for custom vocabularies. +- The builder now accepts a `unknownKeywordFactory`. By default this uses the `UnknownKeywordFactory` implementation that logs a warning and returns a `AnnotationKeyword`. The `DisallowUnknownKeywordFactory` can be used to disallow the use of unknown keywords. +- The implementation of the builder now correctly throws an exception for `$vocabulary` with value of `true` that are not known to the implementation. `ValidatorTypeCode` -* `getNonFormatKeywords` has been removed and replaced with `getKeywords`. This now includes the `format` keyword as the `JsonMetaSchema.Builder` now needs to know if the `format` keyword was configured, as it might not be in meta-schemas that don't define the format vocabulary. -* The applicable `VersionCode` for each of the `ValidatorTypeCode` were modified to remove the keywords that are defined in vocabularies for `Version201909` and `Version202012`. + +- `getNonFormatKeywords` has been removed and replaced with `getKeywords`. This now includes the `format` keyword as the `JsonMetaSchema.Builder` now needs to know if the `format` keyword was configured, as it might not be in meta-schemas that don't define the format vocabulary. +- The applicable `VersionCode` for each of the `ValidatorTypeCode` were modified to remove the keywords that are defined in vocabularies for `Version201909` and `Version202012`. `Vocabulary` -* This now contains `Keyword` instances instead of the string keyword value as it needs to know the explicit implementation. For instance the implementation for the `items` keyword in Draft 2019-09 and Draft 2020-12 are different. -* The following were renamed - * `getId` -> `getIri` + +- This now contains `Keyword` instances instead of the string keyword value as it needs to know the explicit implementation. For instance the implementation for the `items` keyword in Draft 2019-09 and Draft 2020-12 are different. +- The following were renamed + - `getId` -> `getIri` ### 1.3.1 This contains a breaking change in that the results from `failFast` are no longer thrown as an exception. The single result is instead returned normally in the output. This was partially done to distinguish the fail fast result from true exceptions such as when references could not be resolved. -* Annotation collection and reporting has been implemented -* Keywords have been refactored to use annotations for evaluation to improve performance and meet functional requirements -* The list and hierarchical output formats have been implemented as per the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/main/output/jsonschema-validation-output-machines.md). -* The fail fast evaluation processing has been redesigned and fixed. This currently passes the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) with fail fast enabled. Previously contains and union type may cause incorrect results. -* This also contains fixes for regressions introduced in 1.3.0 +- Annotation collection and reporting has been implemented +- Keywords have been refactored to use annotations for evaluation to improve performance and meet functional requirements +- The list and hierarchical output formats have been implemented as per the [Specification for Machine-Readable Output for JSON Schema Validation and Annotation](https://github.com/json-schema-org/json-schema-spec/blob/main/output/jsonschema-validation-output-machines.md). +- The fail fast evaluation processing has been redesigned and fixed. This currently passes the [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) with fail fast enabled. Previously contains and union type may cause incorrect results. +- This also contains fixes for regressions introduced in 1.3.0 The following keywords were refactored to improve performance and meet the functional requirements. In particular this converts the `unevaluatedItems` and `unevaluatedProperties` validators to use annotations to perform the evaluation instead of the current mechanism which affects performance. This also refactors `$recursiveRef` to not rely on that same mechanism. -* `unevaluatedProperties` -* `unevaluatedItems` -* `properties` -* `patternProperties` -* `items` / `additionalItems` -* `prefixItems` / `items` -* `contains` -* `$recursiveRef` +- `unevaluatedProperties` +- `unevaluatedItems` +- `properties` +- `patternProperties` +- `items` / `additionalItems` +- `prefixItems` / `items` +- `contains` +- `$recursiveRef` This also fixes the issue where the `unevaluatedItems` keyword does not take into account the `contains` keyword when performing the evaluation. @@ -170,9 +198,9 @@ This should fix most of the remaining functional and performance issues. #### Functional -| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | -|-----------------|-------------------------------------------------------------------------|-------------------------------------------------------------------|---------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------| -| NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | +| Implementations | Overall | DRAFT_03 | DRAFT_04 | DRAFT_06 | DRAFT_07 | DRAFT_2019_09 | DRAFT_2020_12 | +| --------------- | -------------------------------------------------------------------- | -------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------ | +| NetworkNt | pass: r:4703 (100.0%) o:2369 (100.0%)
fail: r:0 (0.0%) o:1 (0.0%) | | pass: r:600 (100.0%) o:251 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:796 (100.0%) o:318 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:880 (100.0%) o:541 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1201 (100.0%) o:625 (100.0%)
fail: r:0 (0.0%) o:0 (0.0%) | pass: r:1226 (100.0%) o:634 (99.8%)
fail: r:0 (0.0%) o:1 (0.2%) | #### Performance @@ -211,22 +239,24 @@ EveritBenchmark.testValidate:·gc.time thrpt 10 This adds support for Draft 2020-12 This adds support for the following keywords -* `$dynamicRef` -* `$dynamicAnchor` -* `$vocabulary` -This refactors the schema retrieval codes as the ID is based on IRI and not URI. +- `$dynamicRef` +- `$dynamicAnchor` +- `$vocabulary` + +This refactors the schema retrieval codes as the ID is based on IRI and not URI. Note that Java does not support IRIs. See https://cr.openjdk.org/%7Edfuchs/writeups/updating-uri/ for details. The following are removed and replaced by `SchemaLoader` and `SchemaMapper`. -* `URIFactory` - No replacement. The resolve logic is in `AbsoluteIRI`. -* `URISchemeFactory` - No replacement as `URIFactory` isn't required anymore. -* `URISchemeFetcher` - No replacement. The `SchemaLoaders` are iterated and called. -* `URITranslator` - Replaced by `SchemaMapper`. -* `URLFactory` - No replacement as `URIFactory` isn't required anymore. -* `URLFetcher` - Replaced by `UriSchemaLoader`. -* `URNURIFactory` - No replacement as `URIFactory` isn't required anymore. + +- `URIFactory` - No replacement. The resolve logic is in `AbsoluteIRI`. +- `URISchemeFactory` - No replacement as `URIFactory` isn't required anymore. +- `URISchemeFetcher` - No replacement. The `SchemaLoaders` are iterated and called. +- `URITranslator` - Replaced by `SchemaMapper`. +- `URLFactory` - No replacement as `URIFactory` isn't required anymore. +- `URLFetcher` - Replaced by `UriSchemaLoader`. +- `URNURIFactory` - No replacement as `URIFactory` isn't required anymore. The `SchemaLoader` and `SchemaMapper` are configured in the `JsonSchemaFactory.Builder`. See [Customizing Schema Retrieval](schema-retrieval.md). @@ -237,49 +267,59 @@ This can be changed by using a custom meta schema with the relevant `$vocabulary ### 1.2.0 The following are a summary of the changes -* Paths are now specified using the `JsonNodePath`. The paths are `instanceLocation`, `schemaLocation` and `evaluationPath`. The meaning of these paths are as defined in the [specification](https://github.com/json-schema-org/json-schema-spec/blob/main/output/jsonschema-validation-output-machines.md). -* Schema Location comprises an absolute IRI component and a fragment that is a `JsonNodePath` that is typically a JSON pointer -* Rename `at` to `instanceLocation`. Note that for the `required` validator the error message `instanceLocation` does not point to the missing property to be consistent with the [specification](https://json-schema.org/draft/2020-12/json-schema-core#section-12.4.2). The `ValidationMessage` now contains a `property` attribute if this is required. -* Rename `schemaPath` to `schemaLocation`. This should generally be an absolute IRI with a fragment particularly in later drafts. -* Add `evaluationPath` + +- Paths are now specified using the `JsonNodePath`. The paths are `instanceLocation`, `schemaLocation` and `evaluationPath`. The meaning of these paths are as defined in the [specification](https://github.com/json-schema-org/json-schema-spec/blob/main/output/jsonschema-validation-output-machines.md). +- Schema Location comprises an absolute IRI component and a fragment that is a `JsonNodePath` that is typically a JSON pointer +- Rename `at` to `instanceLocation`. Note that for the `required` validator the error message `instanceLocation` does not point to the missing property to be consistent with the [specification](https://json-schema.org/draft/2020-12/json-schema-core#section-12.4.2). The `ValidationMessage` now contains a `property` attribute if this is required. +- Rename `schemaPath` to `schemaLocation`. This should generally be an absolute IRI with a fragment particularly in later drafts. +- Add `evaluationPath` `JsonValidator` -* Now contains `getSchemaLocation` and `getEvaluationPath` in the interface -* Implementations now need a constructor that takes in `schemaLocation` and `evaluationPath` -* The `validate` method uses `JsonNodePath` for the `instanceLocation` -* The `validate` method with just the `rootNode` has been removed + +- Now contains `getSchemaLocation` and `getEvaluationPath` in the interface +- Implementations now need a constructor that takes in `schemaLocation` and `evaluationPath` +- The `validate` method uses `JsonNodePath` for the `instanceLocation` +- The `validate` method with just the `rootNode` has been removed `JsonSchemaWalker` -* The `walk` method uses `JsonNodePath` for the `instanceLocation` + +- The `walk` method uses `JsonNodePath` for the `instanceLocation` `WalkEvent` -* Rename `at` to `instanceLocation` -* Rename `schemaPath` to `schemaLocation` -* Add `evaluationPath` -* Rename `keyWordName` to `keyword` + +- Rename `at` to `instanceLocation` +- Rename `schemaPath` to `schemaLocation` +- Add `evaluationPath` +- Rename `keyWordName` to `keyword` `WalkListenerRunner` -* Rename `at` to `instanceLocation` -* Rename `schemaPath` to `schemaLocation` -* Add `evaluationPath` + +- Rename `at` to `instanceLocation` +- Rename `schemaPath` to `schemaLocation` +- Add `evaluationPath` `BaseJsonValidator` -* The `atPath` methods are removed. Use `JsonNodePath.append` to get the path of the child -* The `buildValidationMessage` methods are removed. Use the `message` builder method instead. + +- The `atPath` methods are removed. Use `JsonNodePath.append` to get the path of the child +- The `buildValidationMessage` methods are removed. Use the `message` builder method instead. `CollectorContext` -* The `evaluatedProperties` and `evaluatedItems` are now `Collection` + +- The `evaluatedProperties` and `evaluatedItems` are now `Collection` `JsonSchema` -* The validator keys are now using `evaluationPath` instead of `schemaPath` -* The `@deprecated` constructor methods have been removed + +- The validator keys are now using `evaluationPath` instead of `schemaPath` +- The `@deprecated` constructor methods have been removed `ValidatorTypeCode` -* The `customMessage` has been removed. This made the `ValidatorTypeCode` mutable if the feature was used as the enum is a shared instance. The logic for determining the `customMessage` has been moved to the validator. -* The creation of `newValidator` instances now uses a functional interface instead of reflection. + +- The `customMessage` has been removed. This made the `ValidatorTypeCode` mutable if the feature was used as the enum is a shared instance. The logic for determining the `customMessage` has been moved to the validator. +- The creation of `newValidator` instances now uses a functional interface instead of reflection. `ValidatorState` -* The `ValidatorState` is now a property of the `ExecutionContext`. This change is largely to improve performance. The `CollectorContext.get` method is particularly slow for this use case. + +- The `ValidatorState` is now a property of the `ExecutionContext`. This change is largely to improve performance. The `CollectorContext.get` method is particularly slow for this use case. ### 1.1.0 @@ -287,11 +327,11 @@ Removes use of `ThreadLocal` to store context and explicitly passes the context The following are the main API changes, typically to accept an `ExecutionContext` as a parameter -* `com.networknt.schema.JsonSchema` -* `com.networknt.schema.JsonValidator` -* `com.networknt.schema.Format` -* `com.networknt.schema.walk.JsonSchemaWalker` -* `com.networknt.schema.walk.WalkEvent` +- `com.networknt.schema.JsonSchema` +- `com.networknt.schema.JsonValidator` +- `com.networknt.schema.Format` +- `com.networknt.schema.walk.JsonSchemaWalker` +- `com.networknt.schema.walk.WalkEvent` `JsonSchema` was modified to optionally accept an `ExecutionContext` for the `validate`, `validateAndCollect` and `walk` methods. For methods where no `ExecutionContext` is supplied, one is created for each run in the `createExecutionContext` method in `JsonSchema`. @@ -299,4 +339,4 @@ The following are the main API changes, typically to accept an `ExecutionContext ### 1.0.82 -Up to version [1.0.81](https://github.com/networknt/json-schema-validator/blob/1.0.81/pom.xml#L99), the dependency `org.apache.commons:commons-lang3` was included as a runtime dependency. Starting with [1.0.82](https://github.com/networknt/json-schema-validator/releases/tag/1.0.82) it is not required anymore. \ No newline at end of file +Up to version [1.0.81](https://github.com/networknt/json-schema-validator/blob/1.0.81/pom.xml#L99), the dependency `org.apache.commons:commons-lang3` was included as a runtime dependency. Starting with [1.0.82](https://github.com/networknt/json-schema-validator/releases/tag/1.0.82) it is not required anymore. diff --git a/doc/walkers.md b/doc/walkers.md index 8cbc6c2f6..ed218ddd9 100644 --- a/doc/walkers.md +++ b/doc/walkers.md @@ -1,291 +1,144 @@ -### JSON Schema Walkers +## Walking Schema -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, or applying default values. JSON walkers were introduced to complement the validation functionality this library already provides. +Provides a mechanism to traverse the schema which allows collecting information, handling cross cutting concerns like logging or instrumentation, or applying default values. This can be done in conjunction to validating the instance if required. -Currently, walking is defined at the validator instance level for all the built-in keywords. +### Walk Listeners -### Walk methods +Developers can define listeners that will be triggered before and after a schema is walked for the following events. -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. +| Event | Description | +| -------- | ------------------------------------------------------------------ | +| Keyword | Triggered before and after each keyword validator is processed. | +| Property | Triggered before an after each schema for a property is processed. | +| Item | Triggered before an after each schema for an item is processed. | -```java -public interface JsonSchemaWalker { - /** - * - * This method gives the capability to walk through the given JsonNode, allowing - * functionality beyond validation like collecting information,handling cross - * cutting concerns like logging or instrumentation. This method also performs - * the validation if {@code shouldValidateSchema} is set to true.
- *
- * {@link BaseJsonValidator#walk(ExecutionContext, JsonNode, JsonNode, JsonNodePath, boolean)} provides - * a default implementation of this method. However validators that parse - * sub-schemas should override this method to call walk method on those - * sub-schemas. - * - * @param executionContext ExecutionContext - * @param node JsonNode - * @param rootNode JsonNode - * @param instanceLocation JsonNodePath - * @param shouldValidateSchema boolean - * @return a set of validation messages if shouldValidateSchema is true. - */ - Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema); -} +### Walk Event -``` +| Name | Description | +| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `executionContext` | Contains details on the current execution. The `collectorContext` on the`executionContext` can be used to store additional details during processing. | +| `schema` | The schema being processed. | +| `keyword` | The keyword being processed. | +| `rootNode` | The root node of the instance document. | +| `instanceNode` | The instance node being processed. | +| `instanceLocation` | The location of the current instance node being processed. | +| `validator` | The keyword validator being processed. | +| `evaluationPath` | The evaluation path. | -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. +### Example -```java - /** - * This is default implementation of walk method. Its job is to call the - * validate method if shouldValidateSchema is enabled. - */ - @Override - default Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - return shouldValidateSchema ? validate(executionContext, node, rootNode, instanceLocation) - : Collections.emptySet(); - } -``` - -A new walk method added to the JSONSchema class allows us to walk through the JSONSchema. +The following example shows how to register a `WalkListener` triggered for keywords using the `WalkConfig`. ```java - public ValidationResult walk(JsonNode node, boolean validate) { - return walk(createExecutionContext(), node, validate); - } - - @Override - public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, - JsonNodePath instanceLocation, boolean shouldValidateSchema) { - Set errors = new LinkedHashSet<>(); - // Walk through all the JSONWalker's. - for (JsonValidator validator : getValidators()) { - JsonNodePath evaluationPathWithKeyword = validator.getEvaluationPath(); - try { - // Call all the pre-walk listeners. If at least one of the pre walk listeners - // returns SKIP, then skip the walk. - if (this.validationContext.getConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, - evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, - this, validator)) { - Set results = null; - try { - results = validator.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); - } finally { - if (results != null && !results.isEmpty()) { - errors.addAll(results); - } - } - } - } finally { - // Call all the post-walk listeners. - this.validationContext.getConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, - evaluationPathWithKeyword.getName(-1), node, rootNode, instanceLocation, - this, validator, errors); +String schemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"description\": \"Default Description\",\r\n" + + " \"properties\": {\r\n" + + " \"tags\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"items\": {\r\n" + + " \"$ref\": \"#/definitions/tag\"\r\n" + + " }\r\n" + + " }\r\n" + + " },\r\n" + + " \"definitions\": {\r\n" + + " \"tag\": {\r\n" + + " \"properties\": {\r\n" + + " \"name\": {\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"description\": {\r\n" + + " \"type\": \"string\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + +KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() + .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() { + @Override + public WalkFlow onWalkStart(WalkEvent walkEvent) { + List propertyKeywords = walkEvent.getExecutionContext() + .getCollectorContext() + .computeIfAbsent("propertyKeywords", key -> new ArrayList<>()); + propertyKeywords.add(walkEvent); + return WalkFlow.CONTINUE; } - } - return errors; - } -``` -Following code snippet shows how to call the walk method on a JsonSchema instance. - -```java -ValidationResult result = jsonSchema.walk(data, false); - -``` - -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. - -### Walk Listeners - -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. -```java -public interface JsonSchemaWalkListener { - - public WalkFlow onWalkStart(WalkEvent walkEvent); - - public void onWalkEnd(WalkEvent walkEvent, Set validationMessages); -} -``` - -Following is the example of a sample WalkListener implementation. - -```java -private static class PropertiesKeywordListener implements JsonSchemaWalkListener { - - @Override - public WalkFlow onWalkStart(WalkEvent keywordWalkEvent) { - JsonNode schemaNode = keywordWalkEvent.getSchema().getSchemaNode(); - if (schemaNode.get("title").textValue().equals("Property3")) { - return WalkFlow.SKIP; + @Override + public void onWalkEnd(WalkEvent walkEvent, List errors) { } - return WalkFlow.CONTINUE; - } - - @Override - public void onWalkEnd(WalkEvent keywordWalkEvent, Set validationMessages) { - - } - } -``` -If the onWalkStart method returns WalkFlow.SKIP, the actual walk method execution will be skipped. - -Walk listeners can be added by using the SchemaValidatorsConfig class. - -```java -SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); - schemaValidatorsConfig.keywordWalkListener(new AllKeywordListener()); - schemaValidatorsConfig.keywordWalkListener(ValidatorTypeCode.REF.getValue(), new RefKeywordListener()); - schemaValidatorsConfig.keywordWalkListener(ValidatorTypeCode.PROPERTIES.getValue(), - new PropertiesKeywordListener()); -final JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) + }) .build(); -this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig.build()); - +Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7).getSchema(schemaData); +String inputData = "{\r\n" + + " \"tags\": [\r\n" + + " {\r\n" + + " \"name\": \"image\",\r\n" + + " \"description\": \"An image\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"name\": \"link\",\r\n" + + " \"description\": \"A link\"\r\n" + + " }\r\n" + + " ]\r\n" + + "}"; +Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext + .walkConfig(walkConfig -> walkConfig.keywordWalkHandler(keywordWalkHandler))); +assertTrue(result.getErrors().isEmpty()); +List propertyKeywords = result.getCollectorContext().get("propertyKeywords"); +assertEquals(3, propertyKeywords.size()); +assertEquals("properties", propertyKeywords.get(0).getValidator().getKeyword()); +assertEquals("", propertyKeywords.get(0).getInstanceLocation().toString()); +assertEquals("/properties", + propertyKeywords.get(0).getEvaluationPath().append(propertyKeywords.get(0).getKeyword()).toString()); +assertEquals("/tags/0", propertyKeywords.get(1).getInstanceLocation().toString()); +assertEquals("image", propertyKeywords.get(1).getInstanceNode().get("name").asText()); +assertEquals("/properties/tags/items/$ref", propertyKeywords.get(1).getEvaluationPath().toString()); +assertEquals("/properties/tags/items/$ref/properties", + propertyKeywords.get(1).getEvaluationPath().append(propertyKeywords.get(1).getKeyword()).toString()); +assertEquals("/tags/1", propertyKeywords.get(2).getInstanceLocation().toString()); +assertEquals("/properties/tags/items/$ref/properties", + propertyKeywords.get(2).getEvaluationPath().append(propertyKeywords.get(2).getKeyword()).toString()); +assertEquals("link", propertyKeywords.get(2).getInstanceNode().get("name").asText()); ``` -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. +### Applying Defaults -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. - -```java -SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); -schemaValidatorsConfig.propertyWalkListener(new ExamplePropertyWalkListener()); -final JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).metaSchema(metaSchema) - .build(); -this.jsonSchema = schemaFactory.getSchema(getSchema(), schemaValidatorsConfig.build()); - -``` - -### Walk Events - -An instance of WalkEvent is passed to both the onWalkStart and onWalkEnd methods of the WalkListeners implementations. - -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. - -Following snippet shows the details captured by WalkEvent instance. - -```java -public class WalkEvent { - private ExecutionContext executionContext; - private JsonSchema schema; - private String keyword; - private JsonNode rootNode; - private JsonNode instanceNode; - private JsonNodePath instanceLocation; - private JsonValidator validator; - ... -} -``` - -### Sample Flow - -Given an example schema as shown, if we write a property listener, the walk flow is as depicted in the image. - -```json -{ - - "title": "Sample Schema", - "definitions" : { - "address" :{ - "street-address": { - "title": "Street Address", - "type": "string" - }, - "pincode": { - "title": "Body", - "type": "integer" - } - } - }, - "properties": { - "name": { - "title": "Title", - "type": "string", - "maxLength": 50 - }, - "body": { - "title": "Body", - "type": "string" - }, - "address": { - "title": "Excerpt", - "$ref": "#/definitions/address" - } - - }, - "additionalProperties": false -} -``` - -![img](walk_flow.png) - - -Few important points to note about the flow. - -1. onWalkStart and onWalkEnd are the methods defined in the property walk listener -2. Anywhere during the flow, onWalkStart can return a WalkFlow.SKIP to stop the walk method execution of a particular "property schema". -3. onWalkEnd will be called even if the onWalkStart returns a WalkFlow.SKIP. -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. - 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. -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, - Our property listener would be invoked for each of the property defined in the "$ref" schema. -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. - Since the walk method will not be called any property or keyword listeners in the "property schema" will not be invoked. - - -### Applying defaults - -In some use cases we may want to apply defaults while walking the schema. -To accomplish this, create an ApplyDefaultsStrategy when creating a SchemaValidatorsConfig. -The input object is changed in place, even if validation fails, or a fail-fast or some other exception is thrown. +In some use cases we may want to apply defaults while walking the schema. To accomplish this, create an `ApplyDefaultsStrategy` when creating a `WalkConfig`. The input object is changed in place, even if validation fails, or a fail-fast or some other exception is thrown. Here is the order of operations in walker. -1. apply defaults -1. run listeners -1. validate if shouldValidateSchema is true -Suppose the JSON schema is -```json -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Schema with default values ", - "type": "object", - "properties": { - "intValue": { - "type": "integer", - "default": 15, - "minimum": 20 - } - }, - "required": ["intValue"] -} -``` - -A JSON file like -```json -{ -} -``` - -would normally fail validation as "intValue" is required. -But if we apply defaults while walking, then required validation passes, and the object is changed in place. +* Apply defaults +* Run listeners +* Validate if `shouldValidateSchema` is true ```java - JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - SchemaValidatorsConfig.Builder schemaValidatorsConfig = SchemaValidatorsConfig.builder(); - schemaValidatorsConfig.applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)); - JsonSchema jsonSchema = schemaFactory.getSchema(SchemaLocation.of("classpath:schema.json"), schemaValidatorsConfig.build()); - - JsonNode inputNode = objectMapper.readTree(getClass().getClassLoader().getResourceAsStream("data.json")); - ValidationResult result = jsonSchema.walk(inputNode, true); - assertThat(result.getValidationMessages(), Matchers.empty()); - assertEquals("{\"intValue\":15}", inputNode.toString()); - assertThat(result.getValidationMessages().stream().map(ValidationMessage::getMessage).collect(Collectors.toList()), - Matchers.containsInAnyOrder("$.intValue: must have a minimum value of 20.")); -``` +String schemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\r\n" + + " \"title\": \"Schema with default values \",\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"intValue\": {\r\n" + + " \"type\": \"integer\",\r\n" + + " \"default\": 15, \r\n" + + " \"minimum\": 20\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\"intValue\"]\r\n" + + "}"; + +String inputData = "{}"; + +SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft4()); +Schema schema = schemaRegistry.getSchema(schemaData); + +JsonNode inputNode = JsonMapperFactory.getInstance().readTree(inputData); +Result result = schema.walk(inputNode, true, executionContext -> executionContext.walkConfig( + walkConfig -> walkConfig.applyDefaultsStrategy(applyDefaultsStrategy -> applyDefaultsStrategy + .applyArrayDefaults(true).applyPropertyDefaults(true).applyPropertyDefaultsIfNull(true)))); +assertFalse(result.getErrors().isEmpty()); +assertEquals("{\"intValue\":15}", inputNode.toString()); +``` \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/Schema.java b/src/main/java/com/networknt/schema/Schema.java index bfd264019..5879b0fe4 100644 --- a/src/main/java/com/networknt/schema/Schema.java +++ b/src/main/java/com/networknt/schema/Schema.java @@ -1474,6 +1474,46 @@ public Result walk(String input, InputFormat inputFormat, boolean validate, Consumer executionCustomizer) { return walk(createExecutionContext(), deserialize(input, inputFormat), validate, executionCustomizer); } + + /** + * Walk the input. + * + * @param input the input + * @param inputFormat the input format + * @param validate true to validate the input against the schema + * @return the validation result + */ + public Result walk(AbsoluteIri input, InputFormat inputFormat, boolean validate) { + return walk(createExecutionContext(), deserialize(input, inputFormat), validate); + } + + /** + * Walk the input. + * + * @param input the input + * @param inputFormat the input format + * @param validate true to validate the input against the schema + * @param executionCustomizer the customizer + * @return the validation result + */ + public Result walk(AbsoluteIri input, InputFormat inputFormat, boolean validate, + ExecutionContextCustomizer executionCustomizer) { + return walk(createExecutionContext(), deserialize(input, inputFormat), validate, executionCustomizer); + } + + /** + * Walk the input. + * + * @param input the input + * @param inputFormat the input format + * @param validate true to validate the input against the schema + * @param executionCustomizer the customizer + * @return the validation result + */ + public Result walk(AbsoluteIri input, InputFormat inputFormat, boolean validate, + Consumer executionCustomizer) { + return walk(createExecutionContext(), deserialize(input, inputFormat), validate, executionCustomizer); + } /** * Walk at the node. @@ -1529,7 +1569,7 @@ public void walk(ExecutionContext executionContext, JsonNode node, JsonNode root try { // Call all the pre-walk listeners. If at least one of the pre walk listeners // returns SKIP, then skip the walk. - if (executionContext.getWalkConfig().getKeywordWalkListenerRunner().runPreWalkListeners(executionContext, + if (executionContext.getWalkConfig().getKeywordWalkHandler().preWalk(executionContext, validator.getKeyword(), node, rootNode, instanceLocation, this, validator)) { executionContext.evaluationPathAddLast(validator.getKeyword()); @@ -1543,7 +1583,7 @@ public void walk(ExecutionContext executionContext, JsonNode node, JsonNode root } } finally { // Call all the post-walk listeners. - executionContext.getWalkConfig().getKeywordWalkListenerRunner().runPostWalkListeners(executionContext, + executionContext.getWalkConfig().getKeywordWalkHandler().postWalk(executionContext, validator.getKeyword(), node, rootNode, instanceLocation, this, validator, executionContext.getErrors().subList(currentErrors, executionContext.getErrors().size())); diff --git a/src/main/java/com/networknt/schema/SchemaRegistry.java b/src/main/java/com/networknt/schema/SchemaRegistry.java index 0b8af9432..8095d4176 100644 --- a/src/main/java/com/networknt/schema/SchemaRegistry.java +++ b/src/main/java/com/networknt/schema/SchemaRegistry.java @@ -36,7 +36,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -651,6 +650,69 @@ public Schema getSchema(final SchemaLocation schemaUri) { return schema; } + /** + * Gets the schema. + * + * @param schemaUri the base absolute IRI + * @param jsonNode the node + * @return the schema + */ + public Schema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { + return newSchema(schemaUri, jsonNode); + } + + /** + * Gets the schema. + * + * @param schemaUri the base absolute IRI + * @param schema the input schema data + * @param inputFormat input format + * @return the schema + */ + public Schema getSchema(final SchemaLocation schemaUri, final String schema, InputFormat inputFormat) { + try { + final JsonNode schemaNode = readTree(schema, inputFormat); + return newSchema(schemaUri, schemaNode); + } catch (IOException ioe) { + logger.error("Failed to load json schema!", ioe); + throw new SchemaException(ioe); + } + } + + /** + * Gets the schema. + * + * @param schemaUri the base absolute IRI + * @param schemaStream the input schema data + * @param inputFormat input format + * @return the schema + */ + public Schema getSchema(final SchemaLocation schemaUri, final InputStream schemaStream, InputFormat inputFormat) { + try { + final JsonNode schemaNode = readTree(schemaStream, inputFormat); + return newSchema(schemaUri, schemaNode); + } catch (IOException ioe) { + logger.error("Failed to load json schema!", ioe); + throw new SchemaException(ioe); + } + } + + /** + * Gets the schema. + *

+ * Using this is not recommended as there is potentially no base IRI for + * resolving references to the absolute IRI. + *

+ * Prefer {@link #getSchema(SchemaLocation, JsonNode)} instead to ensure the + * base IRI if no id is present. + * + * @param jsonNode the node + * @return the schema + */ + public Schema getSchema(final JsonNode jsonNode) { + return newSchema(null, jsonNode); + } + /** * Loads the schema. * @@ -720,56 +782,6 @@ protected Schema getMappedSchema(final SchemaLocation schemaUri) { } } - /** - * Gets the schema. - * - * @param schemaUri the absolute IRI of the schema which can map to the - * retrieval IRI. - * @return the schema - */ - public Schema getSchema(final URI schemaUri) { - return getSchema(SchemaLocation.of(schemaUri.toString())); - } - - /** - * Gets the schema. - * - * @param schemaUri the absolute IRI of the schema which can map to the - * retrieval IRI. - * @param jsonNode the node - * @return the schema - */ - public Schema getSchema(final URI schemaUri, final JsonNode jsonNode) { - return newSchema(SchemaLocation.of(schemaUri.toString()), jsonNode); - } - - /** - * Gets the schema. - * - * @param schemaUri the base absolute IRI - * @param jsonNode the node - * @return the schema - */ - public Schema getSchema(final SchemaLocation schemaUri, final JsonNode jsonNode) { - return newSchema(schemaUri, jsonNode); - } - - /** - * Gets the schema. - *

- * Using this is not recommended as there is potentially no base IRI for - * resolving references to the absolute IRI. - *

- * Prefer {@link #getSchema(SchemaLocation, JsonNode)} instead to ensure the - * base IRI if no id is present. - * - * @param jsonNode the node - * @return the schema - */ - public Schema getSchema(final JsonNode jsonNode) { - return newSchema(null, jsonNode); - } - /** * Gets the schema registry config. * diff --git a/src/main/java/com/networknt/schema/keyword/DependenciesValidator.java b/src/main/java/com/networknt/schema/keyword/DependenciesValidator.java index af18b6293..1acad4aba 100644 --- a/src/main/java/com/networknt/schema/keyword/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/keyword/DependenciesValidator.java @@ -24,6 +24,7 @@ import com.networknt.schema.SchemaContext; import java.util.*; +import java.util.Map.Entry; /** * {@link KeywordValidator} for dependencies. @@ -44,9 +45,10 @@ public DependenciesValidator(SchemaLocation schemaLocation, JsonNode schemaNode, super(KeywordType.DEPENDENCIES, schemaNode, schemaLocation, parentSchema, schemaContext); - for (Iterator it = schemaNode.fieldNames(); it.hasNext(); ) { - String pname = it.next(); - JsonNode pvalue = schemaNode.get(pname); + for (Iterator> it = schemaNode.fields(); it.hasNext(); ) { + Entry entry = it.next(); + String pname = entry.getKey(); + JsonNode pvalue = entry.getValue(); if (pvalue.isArray()) { List depsProps = propertyDeps.get(pname); if (depsProps == null) { diff --git a/src/main/java/com/networknt/schema/keyword/ItemsLegacyValidator.java b/src/main/java/com/networknt/schema/keyword/ItemsLegacyValidator.java index 85e5c7d7e..5c83a5658 100644 --- a/src/main/java/com/networknt/schema/keyword/ItemsLegacyValidator.java +++ b/src/main/java/com/networknt/schema/keyword/ItemsLegacyValidator.java @@ -367,13 +367,13 @@ private void walkSchema(ExecutionContext executionContext, Schema walkSchema, Js executionContext.evaluationPathAddLast(keyword); } try { - boolean executeWalk = executionContext.getWalkConfig().getItemWalkListenerRunner() - .runPreWalkListeners(executionContext, keyword, node, rootNode, instanceLocation, walkSchema, this); + boolean executeWalk = executionContext.getWalkConfig().getItemWalkHandler() + .preWalk(executionContext, keyword, node, rootNode, instanceLocation, walkSchema, this); int currentErrors = executionContext.getErrors().size(); if (executeWalk) { walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } - executionContext.getWalkConfig().getItemWalkListenerRunner().runPostWalkListeners(executionContext, keyword, + executionContext.getWalkConfig().getItemWalkHandler().postWalk(executionContext, keyword, node, rootNode, instanceLocation, walkSchema, this, executionContext.getErrors().subList(currentErrors, executionContext.getErrors().size())); } finally { diff --git a/src/main/java/com/networknt/schema/keyword/ItemsValidator.java b/src/main/java/com/networknt/schema/keyword/ItemsValidator.java index 5877edaf3..5938a247c 100644 --- a/src/main/java/com/networknt/schema/keyword/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/keyword/ItemsValidator.java @@ -146,7 +146,7 @@ private static JsonNode getDefaultNode(Schema schema, ExecutionContext execution private void walkSchema(ExecutionContext executionContext, Schema walkSchema, JsonNode node, JsonNode rootNode, NodePath instanceLocation, boolean shouldValidateSchema) { //@formatter:off - boolean executeWalk = executionContext.getWalkConfig().getItemWalkListenerRunner().runPreWalkListeners( + boolean executeWalk = executionContext.getWalkConfig().getItemWalkHandler().preWalk( executionContext, KeywordType.ITEMS.getValue(), node, @@ -158,7 +158,7 @@ private void walkSchema(ExecutionContext executionContext, Schema walkSchema, Js if (executeWalk) { walkSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } - executionContext.getWalkConfig().getItemWalkListenerRunner().runPostWalkListeners( + executionContext.getWalkConfig().getItemWalkHandler().postWalk( executionContext, KeywordType.ITEMS.getValue(), node, diff --git a/src/main/java/com/networknt/schema/keyword/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/keyword/PatternPropertiesValidator.java index 6b262ed79..6dc0355bc 100644 --- a/src/main/java/com/networknt/schema/keyword/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/keyword/PatternPropertiesValidator.java @@ -26,6 +26,7 @@ import com.networknt.schema.path.NodePath; import com.networknt.schema.regex.RegularExpression; import java.util.*; +import java.util.Map.Entry; /** * {@link KeywordValidator} for patternProperties. @@ -56,11 +57,12 @@ public void validate(ExecutionContext executionContext, JsonNode node, JsonNode return; } Set matchedInstancePropertyNames = null; - Iterator names = node.fieldNames(); + Iterator> fields = node.fields(); boolean collectAnnotations = hasUnevaluatedPropertiesInEvaluationPath(executionContext) || collectAnnotations(executionContext); - while (names.hasNext()) { - String name = names.next(); - JsonNode n = node.get(name); + while (fields.hasNext()) { + Entry field = fields.next(); + String name = field.getKey(); + JsonNode n = field.getValue(); for (Map.Entry entry : schemas.entrySet()) { if (entry.getKey().matches(name)) { NodePath path = instanceLocation.append(name); diff --git a/src/main/java/com/networknt/schema/keyword/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/keyword/PrefixItemsValidator.java index a81f6c60d..0d443b3f4 100644 --- a/src/main/java/com/networknt/schema/keyword/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/keyword/PrefixItemsValidator.java @@ -157,7 +157,7 @@ private void doWalk(ExecutionContext executionContext, int i, private void walkSchema(ExecutionContext executionContext, int schemaIndex, Schema walkSchema, JsonNode node, JsonNode rootNode, NodePath instanceLocation, boolean shouldValidateSchema) { //@formatter:off - boolean executeWalk = executionContext.getWalkConfig().getItemWalkListenerRunner().runPreWalkListeners( + boolean executeWalk = executionContext.getWalkConfig().getItemWalkHandler().preWalk( executionContext, KeywordType.PREFIX_ITEMS.getValue(), node, @@ -174,7 +174,7 @@ private void walkSchema(ExecutionContext executionContext, int schemaIndex, Sche executionContext.evaluationPathRemoveLast(); } } - executionContext.getWalkConfig().getItemWalkListenerRunner().runPostWalkListeners( + executionContext.getWalkConfig().getItemWalkHandler().postWalk( executionContext, KeywordType.PREFIX_ITEMS.getValue(), node, diff --git a/src/main/java/com/networknt/schema/keyword/PropertiesValidator.java b/src/main/java/com/networknt/schema/keyword/PropertiesValidator.java index 884e0e880..603ce880f 100644 --- a/src/main/java/com/networknt/schema/keyword/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/keyword/PropertiesValidator.java @@ -28,7 +28,7 @@ import com.networknt.schema.annotation.Annotation; import com.networknt.schema.path.NodePath; import com.networknt.schema.utils.SchemaRefs; -import com.networknt.schema.walk.WalkListenerRunner; +import com.networknt.schema.walk.WalkHandler; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; @@ -81,7 +81,7 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo entry.getValue().validate(executionContext, propertyNode, rootNode, path); } else { // check if walker is enabled. If it is enabled it is upto the walker implementation to decide about the validation. - walkSchema(executionContext, entry, node, rootNode, instanceLocation, true, executionContext.getWalkConfig().getPropertyWalkListenerRunner()); + walkSchema(executionContext, entry, node, rootNode, instanceLocation, true, executionContext.getWalkConfig().getPropertyWalkHandler()); } } finally { executionContext.evaluationPathRemoveLast(); @@ -96,7 +96,7 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo executionContext.evaluationPathAddLast(entry.getKey()); try { walkSchema(executionContext, entry, node, rootNode, instanceLocation, true, - executionContext.getWalkConfig().getPropertyWalkListenerRunner()); + executionContext.getWalkConfig().getPropertyWalkHandler()); } finally { executionContext.evaluationPathRemoveLast(); } @@ -123,11 +123,11 @@ public void walk(ExecutionContext executionContext, JsonNode node, JsonNode root validate(executionContext, node == null ? MissingNode.getInstance() : node, rootNode, instanceLocation, true); } else { - WalkListenerRunner propertyWalkListenerRunner = executionContext.getWalkConfig().getPropertyWalkListenerRunner(); + WalkHandler propertyWalkHandler = executionContext.getWalkConfig().getPropertyWalkHandler(); for (Map.Entry entry : this.schemas.entrySet()) { executionContext.evaluationPathAddLast(entry.getKey()); try { - walkSchema(executionContext, entry, node, rootNode, instanceLocation, shouldValidateSchema, propertyWalkListenerRunner); + walkSchema(executionContext, entry, node, rootNode, instanceLocation, shouldValidateSchema, propertyWalkHandler); } finally { executionContext.evaluationPathRemoveLast(); } @@ -163,11 +163,11 @@ private static JsonNode getDefaultNode(Schema schema, ExecutionContext execution } private void walkSchema(ExecutionContext executionContext, Map.Entry entry, JsonNode node, - JsonNode rootNode, NodePath instanceLocation, boolean shouldValidateSchema, WalkListenerRunner propertyWalkListenerRunner) { + JsonNode rootNode, NodePath instanceLocation, boolean shouldValidateSchema, WalkHandler propertyWalkHandler) { Schema propertySchema = entry.getValue(); JsonNode propertyNode = (node == null ? null : node.get(entry.getKey())); NodePath path = instanceLocation.append(entry.getKey()); - boolean executeWalk = propertyWalkListenerRunner.runPreWalkListeners(executionContext, + boolean executeWalk = propertyWalkHandler.preWalk(executionContext, KeywordType.PROPERTIES.getValue(), propertyNode, rootNode, path, propertySchema, this); if (propertyNode == null && node != null) { @@ -178,7 +178,7 @@ private void walkSchema(ExecutionContext executionContext, Map.Entry itemWalkListeners; - public ItemWalkListenerRunner(List itemWalkListeners) { + public ItemWalkHandler(List itemWalkListeners) { this.itemWalkListeners = itemWalkListeners; } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public boolean preWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator) { WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); @@ -47,7 +47,7 @@ public boolean runPreWalkListeners(ExecutionContext executionContext, String key } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public void postWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors) { WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); @@ -71,8 +71,8 @@ public Builder itemWalkListeners(Consumer> itemWalkListeners) return this; } - public ItemWalkListenerRunner build() { - return new ItemWalkListenerRunner(itemWalkListeners); + public ItemWalkHandler build() { + return new ItemWalkHandler(itemWalkListeners); } } diff --git a/src/main/java/com/networknt/schema/walk/KeywordWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/KeywordWalkHandler.java similarity index 87% rename from src/main/java/com/networknt/schema/walk/KeywordWalkListenerRunner.java rename to src/main/java/com/networknt/schema/walk/KeywordWalkHandler.java index 9a22e2340..ac0af1897 100644 --- a/src/main/java/com/networknt/schema/walk/KeywordWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/KeywordWalkHandler.java @@ -31,20 +31,20 @@ import com.networknt.schema.path.NodePath; /** - * A {@link WalkListenerRunner} for walking keywords. + * A {@link WalkHandler} for walking keywords. */ -public class KeywordWalkListenerRunner extends AbstractWalkListenerRunner { +public class KeywordWalkHandler extends AbstractWalkHandler { private final List allKeywordWalkListeners; private final Map> keywordWalkListenersMap; - public KeywordWalkListenerRunner(List allKeywordWalkListeners, + public KeywordWalkHandler(List allKeywordWalkListeners, Map> keywordWalkListenersMap) { this.allKeywordWalkListeners = allKeywordWalkListeners; this.keywordWalkListenersMap = keywordWalkListenersMap; } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public boolean preWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator) { boolean continueRunningListenersAndWalk = true; WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, @@ -60,7 +60,7 @@ public boolean runPreWalkListeners(ExecutionContext executionContext, String key } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public void postWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors) { WalkEvent keywordWalkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, @@ -99,8 +99,8 @@ public Builder keywordWalkListeners(Consumer>> ke return this; } - public KeywordWalkListenerRunner build() { - return new KeywordWalkListenerRunner(allKeywordWalkListeners, keywordWalkListeners); + public KeywordWalkHandler build() { + return new KeywordWalkHandler(allKeywordWalkListeners, keywordWalkListeners); } } } diff --git a/src/main/java/com/networknt/schema/walk/PropertyWalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/PropertyWalkHandler.java similarity index 81% rename from src/main/java/com/networknt/schema/walk/PropertyWalkListenerRunner.java rename to src/main/java/com/networknt/schema/walk/PropertyWalkHandler.java index d10fb10bb..889ddd493 100644 --- a/src/main/java/com/networknt/schema/walk/PropertyWalkListenerRunner.java +++ b/src/main/java/com/networknt/schema/walk/PropertyWalkHandler.java @@ -28,18 +28,18 @@ import java.util.function.Consumer; /** - * A {@link WalkListenerRunner} for walking properties. + * A {@link WalkHandler} for walking properties. */ -public class PropertyWalkListenerRunner extends AbstractWalkListenerRunner { +public class PropertyWalkHandler extends AbstractWalkHandler { private final List propertyWalkListeners; - public PropertyWalkListenerRunner(List propertyWalkListeners) { + public PropertyWalkHandler(List propertyWalkListeners) { this.propertyWalkListeners = propertyWalkListeners; } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public boolean preWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator) { WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, schema, validator); @@ -47,7 +47,7 @@ public boolean runPreWalkListeners(ExecutionContext executionContext, String key } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public void postWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors) { WalkEvent walkEvent = constructWalkEvent(executionContext, keyword, instanceNode, rootNode, instanceLocation, @@ -72,8 +72,8 @@ public Builder propertyWalkListeners(Consumer> propertyWalkLi return this; } - public PropertyWalkListenerRunner build() { - return new PropertyWalkListenerRunner(propertyWalkListeners); + public PropertyWalkHandler build() { + return new PropertyWalkHandler(propertyWalkListeners); } } } diff --git a/src/main/java/com/networknt/schema/walk/WalkConfig.java b/src/main/java/com/networknt/schema/walk/WalkConfig.java index d008abad9..813ac0b9b 100644 --- a/src/main/java/com/networknt/schema/walk/WalkConfig.java +++ b/src/main/java/com/networknt/schema/walk/WalkConfig.java @@ -17,6 +17,7 @@ package com.networknt.schema.walk; import java.util.List; +import java.util.function.Consumer; import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.Error; @@ -38,27 +39,27 @@ public static WalkConfig getInstance() { } /** - * {@link WalkListenerRunner} that performs no operations but indicates that it + * {@link WalkHandler} that performs no operations but indicates that it * should walk. */ - public static class NoOpWalkListenerRunner implements WalkListenerRunner { + public static class NoOpWalkHandler implements WalkHandler { private static class Holder { - private static final NoOpWalkListenerRunner INSTANCE = new NoOpWalkListenerRunner(); + private static final NoOpWalkHandler INSTANCE = new NoOpWalkHandler(); } - public static NoOpWalkListenerRunner getInstance() { + public static NoOpWalkHandler getInstance() { return Holder.INSTANCE; } @Override - public boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public boolean preWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator) { // Always walk return true; } @Override - public void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, + public void postWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors) { } @@ -70,19 +71,19 @@ public void runPostWalkListeners(ExecutionContext executionContext, String keywo */ private final ApplyDefaultsStrategy applyDefaultsStrategy; - private final WalkListenerRunner itemWalkListenerRunner; + private final WalkHandler itemWalkHandler; - private final WalkListenerRunner keywordWalkListenerRunner; + private final WalkHandler keywordWalkHandler; - private final WalkListenerRunner propertyWalkListenerRunner; + private final WalkHandler propertyWalkHandler; - WalkConfig(ApplyDefaultsStrategy applyDefaultsStrategy, WalkListenerRunner itemWalkListenerRunner, - WalkListenerRunner keywordWalkListenerRunner, WalkListenerRunner propertyWalkListenerRunner) { + WalkConfig(ApplyDefaultsStrategy applyDefaultsStrategy, WalkHandler itemWalkHandler, + WalkHandler keywordWalkHandler, WalkHandler propertyWalkHandler) { super(); this.applyDefaultsStrategy = applyDefaultsStrategy; - this.itemWalkListenerRunner = itemWalkListenerRunner; - this.keywordWalkListenerRunner = keywordWalkListenerRunner; - this.propertyWalkListenerRunner = propertyWalkListenerRunner; + this.itemWalkHandler = itemWalkHandler; + this.keywordWalkHandler = keywordWalkHandler; + this.propertyWalkHandler = propertyWalkHandler; } /** @@ -95,31 +96,31 @@ public ApplyDefaultsStrategy getApplyDefaultsStrategy() { } /** - * Gets the property walk listener runner. + * Gets the property walk handler. * - * @return the property walk listener runner + * @return the property walk handler */ - public WalkListenerRunner getPropertyWalkListenerRunner() { - return this.propertyWalkListenerRunner; + public WalkHandler getPropertyWalkHandler() { + return this.propertyWalkHandler; } /** - * Gets the item walk listener runner. + * Gets the item walk handler. * - * @return the item walk listener runner + * @return the item walk handler */ - public WalkListenerRunner getItemWalkListenerRunner() { - return this.itemWalkListenerRunner; + public WalkHandler getItemWalkHandler() { + return this.itemWalkHandler; } /** - * Gets the keyword walk listener runner. + * Gets the keyword walk handler. * - * @return the keyword walk listener runner + * @return the keyword walk handler */ - public WalkListenerRunner getKeywordWalkListenerRunner() { - return this.keywordWalkListenerRunner; + public WalkHandler getKeywordWalkHandler() { + return this.keywordWalkHandler; } /** @@ -140,9 +141,9 @@ public static Builder builder() { public static Builder builder(WalkConfig config) { Builder builder = new Builder(); builder.applyDefaultsStrategy = config.applyDefaultsStrategy; - builder.itemWalkListenerRunner = config.itemWalkListenerRunner; - builder.keywordWalkListenerRunner = config.keywordWalkListenerRunner; - builder.propertyWalkListenerRunner = config.propertyWalkListenerRunner; + builder.itemWalkHandler = config.itemWalkHandler; + builder.keywordWalkHandler = config.keywordWalkHandler; + builder.propertyWalkHandler = config.propertyWalkHandler; return builder; } @@ -151,9 +152,9 @@ public static Builder builder(WalkConfig config) { */ public static class Builder { private ApplyDefaultsStrategy applyDefaultsStrategy = null; - private WalkListenerRunner itemWalkListenerRunner = null; - private WalkListenerRunner keywordWalkListenerRunner = null; - private WalkListenerRunner propertyWalkListenerRunner = null; + private WalkHandler itemWalkHandler = null; + private WalkHandler keywordWalkHandler = null; + private WalkHandler propertyWalkHandler = null; /** * Sets the strategy the walker uses to sets nodes to the default value. @@ -168,18 +169,24 @@ public Builder applyDefaultsStrategy(ApplyDefaultsStrategy applyDefaultsStrategy return this; } - public Builder itemWalkListenerRunner(WalkListenerRunner itemWalkListenerRunner) { - this.itemWalkListenerRunner = itemWalkListenerRunner; + public Builder applyDefaultsStrategy(Consumer customizer) { + ApplyDefaultsStrategy.Builder builder = ApplyDefaultsStrategy.builder(applyDefaultsStrategy); + customizer.accept(builder); + return applyDefaultsStrategy(builder.build()); + } + + public Builder itemWalkHandler(WalkHandler itemWalkHandler) { + this.itemWalkHandler = itemWalkHandler; return this; } - public Builder keywordWalkListenerRunner(WalkListenerRunner keywordWalkListenerRunner) { - this.keywordWalkListenerRunner = keywordWalkListenerRunner; + public Builder keywordWalkHandler(WalkHandler keywordWalkHandler) { + this.keywordWalkHandler = keywordWalkHandler; return this; } - public Builder propertyWalkListenerRunner(WalkListenerRunner propertyWalkListenerRunner) { - this.propertyWalkListenerRunner = propertyWalkListenerRunner; + public Builder propertyWalkHandler(WalkHandler propertyWalkHandler) { + this.propertyWalkHandler = propertyWalkHandler; return this; } @@ -187,11 +194,11 @@ public WalkConfig build() { return new WalkConfig( applyDefaultsStrategy != null ? applyDefaultsStrategy : ApplyDefaultsStrategy.EMPTY_APPLY_DEFAULTS_STRATEGY, - itemWalkListenerRunner != null ? itemWalkListenerRunner : NoOpWalkListenerRunner.getInstance(), - keywordWalkListenerRunner != null ? keywordWalkListenerRunner - : NoOpWalkListenerRunner.getInstance(), - propertyWalkListenerRunner != null ? propertyWalkListenerRunner - : NoOpWalkListenerRunner.getInstance()); + itemWalkHandler != null ? itemWalkHandler : NoOpWalkHandler.getInstance(), + keywordWalkHandler != null ? keywordWalkHandler + : NoOpWalkHandler.getInstance(), + propertyWalkHandler != null ? propertyWalkHandler + : NoOpWalkHandler.getInstance()); } } } diff --git a/src/main/java/com/networknt/schema/walk/WalkEvent.java b/src/main/java/com/networknt/schema/walk/WalkEvent.java index cdce63b28..6f5a5b38f 100644 --- a/src/main/java/com/networknt/schema/walk/WalkEvent.java +++ b/src/main/java/com/networknt/schema/walk/WalkEvent.java @@ -99,7 +99,7 @@ public T getValidator() { @Override public String toString() { return "WalkEvent [schemaLocation=" - + getSchema().getSchemaLocation() + ", instanceLocation=" + instanceLocation + "]"; + + getSchema().getSchemaLocation() + ", instanceLocation=" + instanceLocation + ", evaluationPath=" + evaluationPath + "]"; } static class WalkEventBuilder { diff --git a/src/main/java/com/networknt/schema/walk/WalkHandler.java b/src/main/java/com/networknt/schema/walk/WalkHandler.java new file mode 100644 index 000000000..67ee2525e --- /dev/null +++ b/src/main/java/com/networknt/schema/walk/WalkHandler.java @@ -0,0 +1,23 @@ +package com.networknt.schema.walk; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.Schema; +import com.networknt.schema.keyword.KeywordValidator; +import com.networknt.schema.path.NodePath; +import com.networknt.schema.Error; + +import java.util.List; + +/** + * Walk handler that is called before and after visiting. + */ +public interface WalkHandler { + + boolean preWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, + NodePath instanceLocation, Schema schema, KeywordValidator validator); + + void postWalk(ExecutionContext executionContext, String keyword, JsonNode instanceNode, JsonNode rootNode, + NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors); + +} diff --git a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java b/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java deleted file mode 100644 index 66f2a6ad3..000000000 --- a/src/main/java/com/networknt/schema/walk/WalkListenerRunner.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.networknt.schema.walk; - -import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.ExecutionContext; -import com.networknt.schema.Schema; -import com.networknt.schema.keyword.KeywordValidator; -import com.networknt.schema.path.NodePath; -import com.networknt.schema.Error; - -import java.util.List; - -public interface WalkListenerRunner { - - boolean runPreWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, - JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator); - - void runPostWalkListeners(ExecutionContext executionContext, String keyword, JsonNode instanceNode, - JsonNode rootNode, NodePath instanceLocation, Schema schema, KeywordValidator validator, List errors); - -} diff --git a/src/test/java/com/networknt/schema/IfValidatorTest.java b/src/test/java/com/networknt/schema/IfValidatorTest.java index c36b231d5..9f761e735 100644 --- a/src/test/java/com/networknt/schema/IfValidatorTest.java +++ b/src/test/java/com/networknt/schema/IfValidatorTest.java @@ -27,7 +27,7 @@ import com.networknt.schema.keyword.KeywordType; import com.networknt.schema.path.NodePath; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -50,7 +50,7 @@ void walkValidateThen() { + " \"type\": \"number\"\r\n" + " }\r\n" + "}"; - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.TYPE.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -69,7 +69,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); @@ -95,7 +95,7 @@ void walkValidateElse() { + " \"type\": \"number\"\r\n" + " }\r\n" + "}"; - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.TYPE.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -114,7 +114,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); @@ -141,7 +141,7 @@ void walkValidateNull() { + " \"type\": \"number\"\r\n" + " }\r\n" + "}"; - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.TYPE.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -160,7 +160,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); @@ -185,7 +185,7 @@ void walkNoValidate() { + " \"type\": \"number\"\r\n" + " }\r\n" + "}"; - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.TYPE.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -204,7 +204,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); diff --git a/src/test/java/com/networknt/schema/Issue451Test.java b/src/test/java/com/networknt/schema/Issue451Test.java index 229bfc478..ee4e093c1 100644 --- a/src/test/java/com/networknt/schema/Issue451Test.java +++ b/src/test/java/com/networknt/schema/Issue451Test.java @@ -3,7 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.PropertyWalkListenerRunner; +import com.networknt.schema.walk.PropertyWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -58,8 +58,8 @@ private void walk(JsonNode data, boolean shouldValidate) { InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath); Schema schema = getJsonSchemaFromStreamContentV7(schemaInputStream); - WalkConfig walkConfig = WalkConfig.builder().propertyWalkListenerRunner( - PropertyWalkListenerRunner.builder().propertyWalkListener(new CountingWalker()).build()).build(); + WalkConfig walkConfig = WalkConfig.builder().propertyWalkHandler( + PropertyWalkHandler.builder().propertyWalkListener(new CountingWalker()).build()).build(); CollectorContext collectorContext = schema.walk(data, shouldValidate, executionContext -> executionContext.setWalkConfig(walkConfig)).getCollectorContext(); Map collector = (Map) collectorContext.get(COLLECTOR_ID); diff --git a/src/test/java/com/networknt/schema/Issue461Test.java b/src/test/java/com/networknt/schema/Issue461Test.java index 72cbee461..7c28a00ad 100644 --- a/src/test/java/com/networknt/schema/Issue461Test.java +++ b/src/test/java/com/networknt/schema/Issue461Test.java @@ -5,7 +5,7 @@ import com.networknt.schema.keyword.KeywordType; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -25,11 +25,11 @@ protected Schema getJsonSchemaFromStreamContentV7(SchemaLocation schemaUri) { @Test void shouldWalkWithValidation() throws IOException { - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new Walker()) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); Schema schema = getJsonSchemaFromStreamContentV7(SchemaLocation.of("resource:/draft-07/schema#")); diff --git a/src/test/java/com/networknt/schema/Issue467Test.java b/src/test/java/com/networknt/schema/Issue467Test.java index 59259cda0..df9b6dc39 100644 --- a/src/test/java/com/networknt/schema/Issue467Test.java +++ b/src/test/java/com/networknt/schema/Issue467Test.java @@ -34,8 +34,8 @@ import com.networknt.schema.keyword.KeywordType; import com.networknt.schema.path.NodePath; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; -import com.networknt.schema.walk.PropertyWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; +import com.networknt.schema.walk.PropertyWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -49,7 +49,7 @@ class Issue467Test { void shouldWalkKeywordWithValidation() throws URISyntaxException, IOException { InputStream schemaInputStream = Issue467Test.class.getResourceAsStream(schemaPath); final Set properties = new LinkedHashSet<>(); - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -63,7 +63,7 @@ public void onWalkEnd(WalkEvent walkEvent, List set) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); Schema schema = factory.getSchema(schemaInputStream); @@ -78,7 +78,7 @@ public void onWalkEnd(WalkEvent walkEvent, List set) { void shouldWalkPropertiesWithValidation() throws URISyntaxException, IOException { InputStream schemaInputStream = Issue467Test.class.getResourceAsStream(schemaPath); final Set properties = new LinkedHashSet<>(); - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -92,7 +92,7 @@ public void onWalkEnd(WalkEvent walkEvent, List set) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .propertyWalkHandler(propertyWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); Schema schema = factory.getSchema(schemaInputStream); diff --git a/src/test/java/com/networknt/schema/Issue724Test.java b/src/test/java/com/networknt/schema/Issue724Test.java index 13486210a..26bc62504 100644 --- a/src/test/java/com/networknt/schema/Issue724Test.java +++ b/src/test/java/com/networknt/schema/Issue724Test.java @@ -13,7 +13,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -23,7 +23,7 @@ class Issue724Test { @Test void test() throws JsonProcessingException { StringCollector stringCollector = new StringCollector(); - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder().keywordWalkListener(stringCollector).build(); + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder().keywordWalkListener(stringCollector).build(); String schema = "{\n" @@ -51,7 +51,7 @@ void test() throws JsonProcessingException { + " \"billing_address\" : \"my_billing_address\"\n" + "}\n"; WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); Schema jsonSchema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schema); jsonSchema.walk(new ObjectMapper().readTree(data), /* shouldValidateSchema= */ false, executionContext -> executionContext.setWalkConfig(walkConfig)); diff --git a/src/test/java/com/networknt/schema/ItemsLegacyValidatorTest.java b/src/test/java/com/networknt/schema/ItemsLegacyValidatorTest.java index 4c90f6ddb..10af40ea9 100644 --- a/src/test/java/com/networknt/schema/ItemsLegacyValidatorTest.java +++ b/src/test/java/com/networknt/schema/ItemsLegacyValidatorTest.java @@ -30,7 +30,7 @@ import com.networknt.schema.path.NodePath; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.walk.ApplyDefaultsStrategy; -import com.networknt.schema.walk.ItemWalkListenerRunner; +import com.networknt.schema.walk.ItemWalkHandler; import com.networknt.schema.walk.WalkListener; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; @@ -124,7 +124,7 @@ void walk() { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -141,7 +141,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner).build(); + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09); Schema schema = factory.getSchema(schemaData); Result result = schema.walk("[\"the\",\"quick\",\"brown\"]", InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -162,7 +162,7 @@ void walkNull() { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -179,7 +179,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner).build(); + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09); Schema schema = factory.getSchema(schemaData); Result result = schema.walk(null, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -206,7 +206,7 @@ void walkNullTupleItemsAdditional() { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -223,7 +223,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner).build(); + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09); Schema schema = factory.getSchema(schemaData); Result result = schema.walk(null, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -258,7 +258,7 @@ void walkTupleItemsAdditional() throws JsonProcessingException { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -275,7 +275,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner) + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09); Schema schema = factory.getSchema(schemaData); @@ -316,7 +316,7 @@ void walkTupleItemsAdditionalDefaults() throws JsonProcessingException { + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -331,7 +331,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner) + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler) .applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09); Schema schema = factory.getSchema(schemaData); diff --git a/src/test/java/com/networknt/schema/ItemsValidatorTest.java b/src/test/java/com/networknt/schema/ItemsValidatorTest.java index 75d1bed58..c241895fb 100644 --- a/src/test/java/com/networknt/schema/ItemsValidatorTest.java +++ b/src/test/java/com/networknt/schema/ItemsValidatorTest.java @@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test; import com.networknt.schema.path.NodePath; -import com.networknt.schema.walk.ItemWalkListenerRunner; +import com.networknt.schema.walk.ItemWalkHandler; import com.networknt.schema.walk.WalkListener; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; @@ -68,7 +68,7 @@ void walkNull() { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() { + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { return WalkFlow.CONTINUE; @@ -85,7 +85,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { } }).build(); WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); @@ -110,7 +110,7 @@ void walkNullPrefixItems() { + " \"type\": \"string\"\r\n" + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() { + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { return WalkFlow.CONTINUE; @@ -127,7 +127,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { } }).build(); WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) .build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); diff --git a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java index 12d67548b..e3c4d81da 100644 --- a/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkApplyDefaultsTest.java @@ -100,7 +100,7 @@ void testApplyDefaults0(String method) throws IOException { case "walkWithNoDefaults": { // same empty strategy, but tests for NullPointerException WalkConfig walkConfig = WalkConfig.builder() - .applyDefaultsStrategy(null).build(); + .applyDefaultsStrategy((ApplyDefaultsStrategy)null).build(); Schema jsonSchema = createSchema(); errors = jsonSchema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig)).getErrors(); break; diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java index 27ff74ca8..772810f35 100644 --- a/src/test/java/com/networknt/schema/JsonWalkTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkTest.java @@ -12,7 +12,7 @@ import com.networknt.schema.path.NodePath; import com.networknt.schema.keyword.KeywordType; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -49,24 +49,24 @@ void setup() { private void setupSchema() { final Dialect dialect = getDialect(); // Create Schema. - KeywordWalkListenerRunner.Builder keywordWalkListenerRunnerBuilder = KeywordWalkListenerRunner.builder(); + KeywordWalkHandler.Builder keywordWalkHandlerBuilder = KeywordWalkHandler.builder(); - keywordWalkListenerRunnerBuilder.keywordWalkListener(new AllKeywordListener()); - keywordWalkListenerRunnerBuilder.keywordWalkListener(KeywordType.REF.getValue(), new RefKeywordListener()); - keywordWalkListenerRunnerBuilder.keywordWalkListener(KeywordType.PROPERTIES.getValue(), + keywordWalkHandlerBuilder.keywordWalkListener(new AllKeywordListener()); + keywordWalkHandlerBuilder.keywordWalkListener(KeywordType.REF.getValue(), new RefKeywordListener()); + keywordWalkHandlerBuilder.keywordWalkListener(KeywordType.PROPERTIES.getValue(), new PropertiesKeywordListener()); SchemaRegistry schemaFactory = SchemaRegistry.withDialect(dialect); this.jsonSchema = schemaFactory.getSchema(getSchema()); - this.walkConfig = WalkConfig.builder().keywordWalkListenerRunner(keywordWalkListenerRunnerBuilder.build()).build(); + this.walkConfig = WalkConfig.builder().keywordWalkHandler(keywordWalkHandlerBuilder.build()).build(); // Create another Schema. - KeywordWalkListenerRunner.Builder keywordWalkListenerRunner1Builder = KeywordWalkListenerRunner.builder(); - keywordWalkListenerRunner1Builder.keywordWalkListener(KeywordType.REF.getValue(), new RefKeywordListener()); - keywordWalkListenerRunner1Builder.keywordWalkListener(KeywordType.PROPERTIES.getValue(), + KeywordWalkHandler.Builder keywordWalkHandler1Builder = KeywordWalkHandler.builder(); + keywordWalkHandler1Builder.keywordWalkListener(KeywordType.REF.getValue(), new RefKeywordListener()); + keywordWalkHandler1Builder.keywordWalkListener(KeywordType.PROPERTIES.getValue(), new PropertiesKeywordListener()); schemaFactory = SchemaRegistry.withDialect(dialect); this.jsonSchema1 = schemaFactory.getSchema(getSchema()); - this.walkConfig1 = WalkConfig.builder().keywordWalkListenerRunner(keywordWalkListenerRunner1Builder.build()).build(); + this.walkConfig1 = WalkConfig.builder().keywordWalkHandler(keywordWalkHandler1Builder.build()).build(); } private Dialect getDialect() { diff --git a/src/test/java/com/networknt/schema/LocaleTest.java b/src/test/java/com/networknt/schema/LocaleTest.java index c28cf5c1c..863ea0ed7 100644 --- a/src/test/java/com/networknt/schema/LocaleTest.java +++ b/src/test/java/com/networknt/schema/LocaleTest.java @@ -52,19 +52,17 @@ void executionContextLocale() throws JsonMappingException, JsonProcessingExcepti SchemaRegistryConfig config = SchemaRegistryConfig.builder().build(); Schema jsonSchema = getSchema(config); - Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); // fr - ExecutionContext executionContext = jsonSchema.createExecutionContext(); - assertEquals(config.getLocale(), executionContext.getExecutionConfig().getLocale()); - executionContext.executionConfig(executionConfig -> executionConfig.locale(locale)); - List messages = jsonSchema.validate(executionContext, rootNode, OutputFormat.DEFAULT); + List messages = jsonSchema.validate(rootNode, executionContext -> { + Locale locale = Locales.findSupported("it;q=0.9,fr;q=1.0"); // fr + executionContext.executionConfig(executionConfig -> executionConfig.locale(locale)); + }); assertEquals(1, messages.size()); assertEquals("/foo: integer trouvé, string attendu", messages.iterator().next().toString()); - - Locale locale2 = Locales.findSupported("it;q=1.0,fr;q=0.9"); // it - executionContext = jsonSchema.createExecutionContext(); - assertEquals(config.getLocale(), executionContext.getExecutionConfig().getLocale()); - executionContext.executionConfig(executionConfig -> executionConfig.locale(locale2)); - messages = jsonSchema.validate(executionContext, rootNode, OutputFormat.DEFAULT); + + messages = jsonSchema.validate(rootNode, executionContext -> { + Locale locale = Locales.findSupported("it;q=1.0,fr;q=0.9"); // it + executionContext.executionConfig(executionConfig -> executionConfig.locale(locale)); + }); assertEquals(1, messages.size()); assertEquals("/foo: integer trovato, string previsto", messages.iterator().next().toString()); } diff --git a/src/test/java/com/networknt/schema/MessageTest.java b/src/test/java/com/networknt/schema/MessageTest.java index 7a9f42629..5a5e5da1a 100644 --- a/src/test/java/com/networknt/schema/MessageTest.java +++ b/src/test/java/com/networknt/schema/MessageTest.java @@ -71,14 +71,15 @@ public KeywordValidator newValidator(SchemaLocation schemaLocation, @Test void message() { - Dialect dialect = Dialect.builder(Dialects.getDraft202012().getId(), Dialects.getDraft202012()) - .keyword(new EqualsKeyword()).build(); - SchemaRegistry factory = SchemaRegistry.withDialect(dialect); + Dialect dialect = Dialect.builder(Dialects.getDraft202012()) + .keyword(new EqualsKeyword()) + .build(); + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(dialect); String schemaData = "{\r\n" + " \"type\": \"string\",\r\n" + " \"equals\": \"helloworld\"\r\n" + "}"; - Schema schema = factory.getSchema(schemaData); + Schema schema = schemaRegistry.getSchema(schemaData); List messages = schema.validate("\"helloworlda\"", InputFormat.JSON); assertEquals(1, messages.size()); assertEquals(": must be equal to 'helloworld'", messages.iterator().next().toString()); diff --git a/src/test/java/com/networknt/schema/MetaSchemaValidationTest.java b/src/test/java/com/networknt/schema/MetaSchemaValidationTest.java index f7443d6c0..6768a04f5 100644 --- a/src/test/java/com/networknt/schema/MetaSchemaValidationTest.java +++ b/src/test/java/com/networknt/schema/MetaSchemaValidationTest.java @@ -39,10 +39,9 @@ class MetaSchemaValidationTest { void oas31() throws IOException { try (InputStream input = MetaSchemaValidationTest.class.getResourceAsStream("/schema/oas/3.1/petstore.json")) { JsonNode inputData = JsonMapperFactory.getInstance().readTree(input); - SchemaRegistryConfig config = SchemaRegistryConfig.builder().build(); Schema schema = SchemaRegistry .withDefaultDialect(SpecificationVersion.DRAFT_2020_12, - builder -> builder.schemaRegistryConfig(config).schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + builder -> builder.schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers .mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1"))) .getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07")); List messages = schema.validate(inputData); diff --git a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java index dc1281286..eb9754e20 100644 --- a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java +++ b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java @@ -10,7 +10,7 @@ import com.networknt.schema.path.NodePath; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.walk.ApplyDefaultsStrategy; -import com.networknt.schema.walk.ItemWalkListenerRunner; +import com.networknt.schema.walk.ItemWalkHandler; import com.networknt.schema.walk.WalkListener; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; @@ -133,7 +133,7 @@ void walkNull() { + " }\n" + " ]\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() { + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { return WalkFlow.CONTINUE; @@ -149,7 +149,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { items.add(walkEvent); } }).build(); - WalkConfig walkConfig = WalkConfig.builder().itemWalkListenerRunner(itemWalkListenerRunner).build(); + WalkConfig walkConfig = WalkConfig.builder().itemWalkHandler(itemWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); @@ -190,7 +190,7 @@ void walkDefaults() throws JsonProcessingException { + " ]\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder() + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder() .itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -206,7 +206,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { } }).build(); WalkConfig walkConfig = WalkConfig.builder().applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)) - .itemWalkListenerRunner(itemWalkListenerRunner).build(); + .itemWalkHandler(itemWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12); Schema schema = factory.getSchema(schemaData); diff --git a/src/test/java/com/networknt/schema/QuickStartTest.java b/src/test/java/com/networknt/schema/QuickStartTest.java index 6b10c883d..3315ce297 100644 --- a/src/test/java/com/networknt/schema/QuickStartTest.java +++ b/src/test/java/com/networknt/schema/QuickStartTest.java @@ -1,7 +1,10 @@ package com.networknt.schema; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -9,10 +12,14 @@ import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.dialect.Dialects; +import com.networknt.schema.output.OutputUnit; +import com.networknt.schema.regex.JoniRegularExpressionFactory; import com.networknt.schema.serialization.JsonMapperFactory; +import com.networknt.schema.utils.JsonNodes; /** * Quick start test. @@ -84,6 +91,150 @@ void addressExample() { assertEquals("required", errors.get(0).getKeyword()); } + @Test + void readme() { + /* + * The SchemaRegistryConfig can be optionally used to configure certain aspects + * of how the validation is performed. + * + * By default the JDK regular expression implementation which is not ECMA 262 + * compliant is used. The GraalJSRegularExpressionFactory.getInstance() offers + * the best compliance followed by JoniRegularExpressionFactory.getInstance() + * but both require additional optional dependencies. + */ + SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(JoniRegularExpressionFactory.getInstance()).build(); + + /* + * This creates a schema registry that supports all the standard dialects for + * cross-dialect validation and will use Draft 2020-12 as the default if $schema + * is not specified in the schema data. If $schema is specified in the schema + * data then that schema dialect will be used instead and this version is + * ignored. + */ + SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12, + builder -> builder.schemaRegistryConfig(schemaRegistryConfig) + /* + * This creates a mapping from $id which starts with + * https://www.example.org/schema to the retrieval IRI classpath:schema. + */ + .schemaIdResolvers(schemaIdResolvers -> schemaIdResolvers + .mapPrefix("https://www.example.com/schema", "classpath:schema"))); + + /* + * Due to the mapping the schema will be retrieved from the classpath at + * classpath:schema/example-main.json. If the schema data does not specify an + * $id the absolute IRI of the schema location will be used as the $id. If the + * schema data does not specify a dialect using $schema the default dialect + * specified when creating the schema registry. + */ + Schema schema = schemaRegistry.getSchema(SchemaLocation.of("https://www.example.com/schema/example-main.json")); + String input = "{\r\n" + + " \"main\": {\r\n" + + " \"common\": {\r\n" + + " \"field\": \"invalidfield\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + List errors = schema.validate(input, InputFormat.JSON, executionContext -> { + /* + * By default since Draft 2019-09 the format keyword only generates annotations + * and not assertions. + */ + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); + }); + assertEquals(2, errors.size()); + } + + @Test + void readmeMetaSchema() { + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012()); + /* + * Due to the mapping the meta-schema for the dialect will be retrieved from the + * classpath at classpath:draft/2020-12/schema. + */ + Schema schema = schemaRegistry.getSchema(SchemaLocation.of(Dialects.getDraft202012().getId())); + String input = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"key\": {\r\n" + + " \"title\" : \"My key\",\r\n" + + " \"type\": \"invalidtype\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + List errors = schema.validate(input, InputFormat.JSON, executionContext -> { + /* + * By default since Draft 2019-09 the format keyword only generates annotations + * and not assertions. + */ + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); + }); + assertEquals(2, errors.size()); + } + + @Test + void location() { + String schemaData = "{\r\n" + + " \"$id\": \"https://schema/myschema\",\r\n" + + " \"properties\": {\r\n" + + " \"startDate\": {\r\n" + + " \"format\": \"date\",\r\n" + + " \"minLength\": 6\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + String inputData = "{\r\n" + + " \"startDate\": \"1\"\r\n" + + "}"; + + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.nodeReader(nodeReader -> nodeReader.locationAware())); + + Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON); + List errors = schema.validate(inputData, InputFormat.JSON, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); + }); + Error format = errors.get(0); + JsonLocation formatInstanceNodeTokenLocation = JsonNodes.tokenStreamLocationOf(format.getInstanceNode()); + JsonLocation formatSchemaNodeTokenLocation = JsonNodes.tokenStreamLocationOf(format.getSchemaNode()); + Error minLength = errors.get(1); + JsonLocation minLengthInstanceNodeTokenLocation = JsonNodes.tokenStreamLocationOf(minLength.getInstanceNode()); + JsonLocation minLengthSchemaNodeTokenLocation = JsonNodes.tokenStreamLocationOf(minLength.getSchemaNode()); + + assertEquals("format", format.getKeyword()); + assertEquals("date", format.getSchemaNode().asText()); + assertEquals(5, formatSchemaNodeTokenLocation.getLineNr()); + assertEquals(17, formatSchemaNodeTokenLocation.getColumnNr()); + assertEquals("1", format.getInstanceNode().asText()); + assertEquals(2, formatInstanceNodeTokenLocation.getLineNr()); + assertEquals(16, formatInstanceNodeTokenLocation.getColumnNr()); + assertEquals("minLength", minLength.getKeyword()); + assertEquals("6", minLength.getSchemaNode().asText()); + assertEquals(6, minLengthSchemaNodeTokenLocation.getLineNr()); + assertEquals(20, minLengthSchemaNodeTokenLocation.getColumnNr()); + assertEquals("1", minLength.getInstanceNode().asText()); + assertEquals(2, minLengthInstanceNodeTokenLocation.getLineNr()); + assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); + assertEquals(16, minLengthInstanceNodeTokenLocation.getColumnNr()); + } + + @Test + void annotation() { + String inputData = "{ \"hello\": \"world\"}"; + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft201909()); + Schema schema = schemaRegistry.getSchema(SchemaLocation.of("classpath:schema/example-ref.json")); + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig + .annotationCollectionEnabled(true) + .annotationCollectionFilter(keyword -> true) + .formatAssertionsEnabled(true)); + }); + assertNotNull(outputUnit); + } + @Test void schemaFromSchemaLocationMapping() { SchemaRegistry schemaRegistry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12, @@ -193,4 +344,33 @@ void schemaFromJsonNode() throws JsonProcessingException { .executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true))); assertEquals(1, errors.size()); } + + @Test + void defaults() throws IOException { + String schemaData = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\r\n" + + " \"title\": \"Schema with default values \",\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"intValue\": {\r\n" + + " \"type\": \"integer\",\r\n" + + " \"default\": 15, \r\n" + + " \"minimum\": 20\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\"intValue\"]\r\n" + + "}"; + + String inputData = "{}"; + + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft4()); + Schema schema = schemaRegistry.getSchema(schemaData); + + JsonNode inputNode = JsonMapperFactory.getInstance().readTree(inputData); + Result result = schema.walk(inputNode, true, executionContext -> executionContext.walkConfig( + walkConfig -> walkConfig.applyDefaultsStrategy(applyDefaultsStrategy -> applyDefaultsStrategy + .applyArrayDefaults(true).applyPropertyDefaults(true).applyPropertyDefaultsIfNull(true)))); + assertFalse(result.getErrors().isEmpty()); + assertEquals("{\"intValue\":15}", inputNode.toString()); + } } diff --git a/src/test/java/com/networknt/schema/SharedConfigTest.java b/src/test/java/com/networknt/schema/SharedConfigTest.java index edd62b88d..ea4002a7b 100644 --- a/src/test/java/com/networknt/schema/SharedConfigTest.java +++ b/src/test/java/com/networknt/schema/SharedConfigTest.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.walk.WalkListener; -import com.networknt.schema.walk.KeywordWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -34,9 +34,9 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { void shouldCallAllKeywordListenerOnWalkStart() throws Exception { AllKeywordListener allKeywordListener = new AllKeywordListener(); - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(allKeywordListener).build(); - WalkConfig walkConfig = WalkConfig.builder().keywordWalkListenerRunner(keywordWalkListenerRunner).build(); + WalkConfig walkConfig = WalkConfig.builder().keywordWalkHandler(keywordWalkHandler).build(); SchemaRegistry factory = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_7); diff --git a/src/test/java/com/networknt/schema/keyword/PropertiesValidatorTest.java b/src/test/java/com/networknt/schema/keyword/PropertiesValidatorTest.java index 6cbbfc600..6fe6bdcd6 100644 --- a/src/test/java/com/networknt/schema/keyword/PropertiesValidatorTest.java +++ b/src/test/java/com/networknt/schema/keyword/PropertiesValidatorTest.java @@ -10,8 +10,8 @@ import com.networknt.schema.SpecificationVersion; import com.networknt.schema.dialect.Dialects; import com.networknt.schema.walk.ApplyDefaultsStrategy; -import com.networknt.schema.walk.KeywordWalkListenerRunner; -import com.networknt.schema.walk.PropertyWalkListenerRunner; +import com.networknt.schema.walk.KeywordWalkHandler; +import com.networknt.schema.walk.PropertyWalkHandler; import com.networknt.schema.walk.WalkConfig; import com.networknt.schema.walk.WalkEvent; import com.networknt.schema.walk.WalkFlow; @@ -66,7 +66,7 @@ void evaluationPath() { @Test void evaluationPathWalk() { - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -77,7 +77,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { } }).build(); - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -109,8 +109,8 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { Schema schema = schemaRegistry.getSchema(schemaData, InputFormat.JSON); Result result = schema.walk(instanceData, InputFormat.JSON, true, executionContext -> executionContext - .walkConfig(walkConfig -> walkConfig.propertyWalkListenerRunner(propertyWalkListenerRunner) - .keywordWalkListenerRunner(keywordWalkListenerRunner))); + .walkConfig(walkConfig -> walkConfig.propertyWalkHandler(propertyWalkHandler) + .keywordWalkHandler(keywordWalkHandler))); List errors = result.getErrors(); assertEquals(2, errors.size()); assertEquals("/properties/productId/minimum", errors.get(0).getEvaluationPath().toString()); diff --git a/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java b/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java new file mode 100644 index 000000000..54bc3ae38 --- /dev/null +++ b/src/test/java/com/networknt/schema/regex/RegularExpressionTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.networknt.schema.regex; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.Error; +import com.networknt.schema.InputFormat; +import com.networknt.schema.Schema; +import com.networknt.schema.SchemaRegistry; +import com.networknt.schema.SchemaRegistryConfig; +import com.networknt.schema.dialect.Dialects; + +/** + * RegularExpressionTest. + */ +public class RegularExpressionTest { + @Test + public void testInvalidRegexValidatorECMA262() throws Exception { + SchemaRegistryConfig schemaRegistryConfig = SchemaRegistryConfig.builder() + .regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()).build(); + SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft202012(), + builder -> builder.schemaRegistryConfig(schemaRegistryConfig)); + Schema schema = schemaRegistry.getSchema("{\r\n" + + " \"format\": \"regex\"\r\n" + + "}"); + List errors = schema.validate("\"\\\\a\"", InputFormat.JSON, executionContext -> { + executionContext.executionConfig(executionConfig -> executionConfig.formatAssertionsEnabled(true)); + }); + assertFalse(errors.isEmpty()); + } +} \ No newline at end of file diff --git a/src/test/java/com/networknt/schema/walk/WalkListenerTest.java b/src/test/java/com/networknt/schema/walk/WalkListenerTest.java index dc8a82e69..efc693ccd 100644 --- a/src/test/java/com/networknt/schema/walk/WalkListenerTest.java +++ b/src/test/java/com/networknt/schema/walk/WalkListenerTest.java @@ -43,7 +43,6 @@ import com.networknt.schema.keyword.ItemsLegacyValidator; import com.networknt.schema.keyword.ItemsValidator; import com.networknt.schema.keyword.PropertiesValidator; -import com.networknt.schema.path.NodePath; import com.networknt.schema.keyword.KeywordType; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.utils.JsonNodes; @@ -84,14 +83,12 @@ void keywordListener() { + " }\r\n" + "}"; - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - @SuppressWarnings("unchecked") - List propertyKeywords = (List) walkEvent.getExecutionContext() + List propertyKeywords = walkEvent.getExecutionContext() .getCollectorContext() - .getData() .computeIfAbsent("propertyKeywords", key -> new ArrayList<>()); propertyKeywords.add(walkEvent); return WalkFlow.CONTINUE; @@ -115,27 +112,23 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { + " }\r\n" + " ]\r\n" + "}"; - WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) - .build(); - Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); + Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext + .walkConfig(walkConfig -> walkConfig.keywordWalkHandler(keywordWalkHandler))); assertTrue(result.getErrors().isEmpty()); - @SuppressWarnings("unchecked") - List propertyKeywords = (List) result.getExecutionContext().getCollectorContext().get("propertyKeywords"); + List propertyKeywords = result.getCollectorContext().get("propertyKeywords"); assertEquals(3, propertyKeywords.size()); assertEquals("properties", propertyKeywords.get(0).getValidator().getKeyword()); assertEquals("", propertyKeywords.get(0).getInstanceLocation().toString()); - //assertEquals("/properties", propertyKeywords.get(0).getEvaluationPath() - // .append(propertyKeywords.get(0).getKeyword()).toString()); + assertEquals("/properties", + propertyKeywords.get(0).getEvaluationPath().append(propertyKeywords.get(0).getKeyword()).toString()); assertEquals("/tags/0", propertyKeywords.get(1).getInstanceLocation().toString()); assertEquals("image", propertyKeywords.get(1).getInstanceNode().get("name").asText()); - //assertEquals("/properties/tags/items/$ref/properties", - // propertyKeywords.get(1).getValidator().getEvaluationPath().toString()); - //assertEquals("/properties/tags/items/$ref/properties", propertyKeywords.get(1).getEvaluationPath() - // .append(propertyKeywords.get(1).getKeyword()).toString()); + assertEquals("/properties/tags/items/$ref", propertyKeywords.get(1).getEvaluationPath().toString()); + assertEquals("/properties/tags/items/$ref/properties", + propertyKeywords.get(1).getEvaluationPath().append(propertyKeywords.get(1).getKeyword()).toString()); assertEquals("/tags/1", propertyKeywords.get(2).getInstanceLocation().toString()); - //assertEquals("/properties/tags/items/$ref/properties", propertyKeywords.get(2).getEvaluationPath() - // .append(propertyKeywords.get(2).getKeyword()).toString()); + assertEquals("/properties/tags/items/$ref/properties", + propertyKeywords.get(2).getEvaluationPath().append(propertyKeywords.get(2).getKeyword()).toString()); assertEquals("link", propertyKeywords.get(2).getInstanceNode().get("name").asText()); } @@ -167,14 +160,12 @@ void propertyListener() { + " }\r\n" + "}"; - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - @SuppressWarnings("unchecked") - List properties = (List) walkEvent.getExecutionContext() + List properties = walkEvent.getExecutionContext() .getCollectorContext() - .getData() .computeIfAbsent("properties", key -> new ArrayList<>()); properties.add(walkEvent); return WalkFlow.CONTINUE; @@ -199,7 +190,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { + " ]\r\n" + "}"; WalkConfig walkConfig = WalkConfig.builder() - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .propertyWalkHandler(propertyWalkHandler) .build(); Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); assertTrue(result.getErrors().isEmpty()); @@ -257,13 +248,11 @@ void itemsListener() { + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() { + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - @SuppressWarnings("unchecked") - List items = (List) walkEvent.getExecutionContext() + List items = walkEvent.getExecutionContext() .getCollectorContext() - .getData() .computeIfAbsent("items", key -> new ArrayList<>()); items.add(walkEvent); return WalkFlow.CONTINUE; @@ -287,7 +276,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { + " ]\r\n" + "}"; WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) .build(); Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -334,13 +323,11 @@ void items202012Listener() { + " }\r\n" + "}"; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(new WalkListener() { + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - @SuppressWarnings("unchecked") - List items = (List) walkEvent.getExecutionContext() + List items = walkEvent.getExecutionContext() .getCollectorContext() - .getData() .computeIfAbsent("items", key -> new ArrayList<>()); items.add(walkEvent); return WalkFlow.CONTINUE; @@ -364,13 +351,12 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { + " ]\r\n" + "}"; WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) .build(); Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); assertTrue(result.getErrors().isEmpty()); - @SuppressWarnings("unchecked") - List items = (List) result.getExecutionContext().getCollectorContext().get("items"); + List items = result.getExecutionContext().getCollectorContext().get("items"); assertEquals(2, items.size()); assertEquals("items", items.get(0).getValidator().getKeyword()); assertInstanceOf(ItemsValidator.class, items.get(0).getValidator()); @@ -384,14 +370,12 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { @Test void draft201909() { - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { - @SuppressWarnings("unchecked") - List propertyKeywords = (List) walkEvent.getExecutionContext() + List propertyKeywords = walkEvent.getExecutionContext() .getCollectorContext() - .getData() .computeIfAbsent("propertyKeywords", key -> new ArrayList<>()); propertyKeywords.add(walkEvent); return WalkFlow.CONTINUE; @@ -421,7 +405,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { + " }\r\n" + "}"; WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); Result result = schema.walk(inputData, InputFormat.JSON, true, executionContext -> executionContext.setWalkConfig(walkConfig)); assertTrue(result.getErrors().isEmpty()); @@ -566,8 +550,10 @@ void applyDefaults() throws JsonProcessingException { + " }\r\n" + "}"; - WalkConfig walkConfig = WalkConfig.builder() - .applyDefaultsStrategy(new ApplyDefaultsStrategy(true, true, true)).build(); + WalkConfig walkConfig = WalkConfig.builder() + .applyDefaultsStrategy(applyDefaultsStrategy -> applyDefaultsStrategy.applyArrayDefaults(true) + .applyPropertyDefaults(true).applyPropertyDefaultsIfNull(true)) + .build(); Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData); JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}"); Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -598,7 +584,7 @@ void applyDefaultsWithWalker() throws JsonProcessingException { + " }\r\n" + "}"; - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -627,7 +613,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData); WalkConfig walkConfig = WalkConfig.builder() - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .propertyWalkHandler(propertyWalkHandler) .build(); JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}"); Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig)); @@ -658,7 +644,7 @@ void applyInvalidDefaultsWithWalker() throws JsonProcessingException { + " }\r\n" + "}"; - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -688,7 +674,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData); JsonNode inputNode = JsonMapperFactory.getInstance().readTree("{}"); WalkConfig walkConfig = WalkConfig.builder() - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .propertyWalkHandler(propertyWalkHandler) .build(); Result result = schema.walk(inputNode, true, executionContext -> executionContext.setWalkConfig(walkConfig)); assertEquals("{\"s\":1,\"ref\":\"REF\"}", inputNode.toString()); @@ -721,7 +707,7 @@ void missingRequired() throws JsonProcessingException { + " }\r\n" + "}"; Map missingSchemaNode = new LinkedHashMap<>(); - KeywordWalkListenerRunner keywordWalkListenerRunner = KeywordWalkListenerRunner.builder() + KeywordWalkHandler keywordWalkHandler = KeywordWalkHandler.builder() .keywordWalkListener(KeywordType.PROPERTIES.getValue(), new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -756,7 +742,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .keywordWalkListenerRunner(keywordWalkListenerRunner) + .keywordWalkHandler(keywordWalkHandler) .build(); Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData); @@ -796,7 +782,7 @@ void generateDataWithWalker() throws JsonProcessingException { + " }\r\n" + "}"; - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder() + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder() .propertyWalkListener(new WalkListener() { @Override public WalkFlow onWalkStart(WalkEvent walkEvent) { @@ -815,7 +801,7 @@ public WalkFlow onWalkStart(WalkEvent walkEvent) { String faker = fakerNode.asText(); String fakeData = generators.get(faker).get(); JsonNode fakeDataNode = JsonNodeFactory.instance.textNode(fakeData); - ObjectNode parentNode = (ObjectNode) JsonNodes.get(walkEvent.getRootNode(), + ObjectNode parentNode = JsonNodes.get(walkEvent.getRootNode(), walkEvent.getInstanceLocation().getParent()); parentNode.set(walkEvent.getInstanceLocation().getName(-1), fakeDataNode); } @@ -829,7 +815,7 @@ public void onWalkEnd(WalkEvent walkEvent, List errors) { }) .build(); WalkConfig walkConfig = WalkConfig.builder() - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .propertyWalkHandler(propertyWalkHandler) .build(); Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData); @@ -871,25 +857,22 @@ public WalkFlow onWalkStart(WalkEvent walkEvent) { @Override public void onWalkEnd(WalkEvent walkEvent, List errors) { - @SuppressWarnings("unchecked") - List items = (List) walkEvent.getExecutionContext() + List items = walkEvent.getExecutionContext() .getCollectorContext() - .getData() - .computeIfAbsent("items", key -> new ArrayList()); + .computeIfAbsent("items", key -> new ArrayList<>()); items.add(walkEvent); } }; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(listener).build(); - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(listener).build(); + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(listener).build(); + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(listener).build(); WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) + .propertyWalkHandler(propertyWalkHandler) .build(); Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2019_09).getSchema(schemaData); Result result = schema.walk(null, true, executionContext -> executionContext.setWalkConfig(walkConfig)); - @SuppressWarnings("unchecked") - List items = (List) result.getExecutionContext().getCollectorContext().get("items"); + List items = result.getExecutionContext().getCollectorContext().get("items"); assertEquals(4, items.size()); assertEquals("/name", items.get(0).getInstanceLocation().toString()); assertEquals("properties", items.get(0).getKeyword()); @@ -937,19 +920,17 @@ public WalkFlow onWalkStart(WalkEvent walkEvent) { @Override public void onWalkEnd(WalkEvent walkEvent, List errors) { - @SuppressWarnings("unchecked") - List items = (List) walkEvent.getExecutionContext() + List items = walkEvent.getExecutionContext() .getCollectorContext() - .getData() - .computeIfAbsent("items", key -> new ArrayList()); + .computeIfAbsent("items", key -> new ArrayList<>()); items.add(walkEvent); } }; - ItemWalkListenerRunner itemWalkListenerRunner = ItemWalkListenerRunner.builder().itemWalkListener(listener).build(); - PropertyWalkListenerRunner propertyWalkListenerRunner = PropertyWalkListenerRunner.builder().propertyWalkListener(listener).build(); + ItemWalkHandler itemWalkHandler = ItemWalkHandler.builder().itemWalkListener(listener).build(); + PropertyWalkHandler propertyWalkHandler = PropertyWalkHandler.builder().propertyWalkListener(listener).build(); WalkConfig walkConfig = WalkConfig.builder() - .itemWalkListenerRunner(itemWalkListenerRunner) - .propertyWalkListenerRunner(propertyWalkListenerRunner) + .itemWalkHandler(itemWalkHandler) + .propertyWalkHandler(propertyWalkHandler) .build(); Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12).getSchema(schemaData);