diff --git a/CHANGELOG.md b/CHANGELOG.md index a99331834..43b564cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # Change Log All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/) -and this project adheres to [Semantic Versioning](http://semver.org/). +This format is based on [Keep a Changelog](http://keepachangelog.com/). + +This project does not adhere to [Semantic Versioning](https://semver.org/) and minor version changes can have incompatible API changes. These incompatible API changes will largely affect those who have custom validator or walker implementations. Those who just use the library to validate using the standard JSON Schema Draft specifications may not need changes. ## [Unreleased] diff --git a/README.md b/README.md index 621837844..c332e39b3 100644 --- a/README.md +++ b/README.md @@ -11,36 +11,91 @@ [![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](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. -In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The default JSON parser is the [Jackson](https://github.com/FasterXML/jackson) that is the most popular one. 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. +In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). 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 Draft Specification Compatibility +## JSON Schema Specification compatibility 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 -Information on notable or breaking changes when upgrading the library can be found in the [Upgrading to new versions](doc/upgrading.md) document. This library can contain breaking changes in minor version releases. +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. -For the latest version, please check 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) + +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 -It is the fastest Java JSON Schema Validator as far as I know. Here is the testing result compare with the other two open-source implementations. It is about 32 times faster than the Fge and five times faster than the Everit. +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. -- fge: 7130ms -- everit-org: 1168ms -- networknt: 223ms +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 -You can run the performance tests for three libraries from [https://github.com/networknt/json-schema-validator-perftest](https://github.com/networknt/json-schema-validator-perftest) +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.3.1 + +``` +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.testValidate thrpt 10 6776.693 ± 115.309 ops/s +NetworkntBenchmark.testValidate:·gc.alloc.rate thrpt 10 971.191 ± 16.420 MB/sec +NetworkntBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 165318.816 ± 0.459 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 968.894 ± 51.234 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 164933.962 ± 8636.203 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.002 ± 0.001 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.274 ± 0.218 B/op +NetworkntBenchmark.testValidate:·gc.count thrpt 10 89.000 counts +NetworkntBenchmark.testValidate:·gc.time thrpt 10 99.000 ms +``` + +###### Everit 1.14.1 + +``` +Benchmark Mode Cnt Score Error Units +EveritBenchmark.testValidate thrpt 10 3719.192 ± 125.592 ops/s +EveritBenchmark.testValidate:·gc.alloc.rate thrpt 10 1448.208 ± 74.746 MB/sec +EveritBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 449621.927 ± 7400.825 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 1446.397 ± 79.919 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 449159.799 ± 18614.931 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.001 ± 0.001 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.364 ± 0.391 B/op +EveritBenchmark.testValidate:·gc.count thrpt 10 133.000 counts +EveritBenchmark.testValidate:·gc.time thrpt 10 148.000 ms +``` -#### Parser +#### Functionality -It uses Jackson that is the most popular JSON parser in Java. If you are using Jackson parser already in your project, it is natural to choose this library over others for schema validation. +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: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%) | + +* Note that this uses the ECMA 262 Validator option turned on for the `pattern` 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 @@ -50,9 +105,11 @@ The library works with JSON and YAML on both schema definitions and input data. 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. -#### Dependency +#### Minimal Dependencies -Following the design principle of the Light Platform, this library has minimum dependencies to ensure there are no dependency conflicts when using it. +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. @@ -86,6 +143,8 @@ The following are the dependencies that will automatically be included when this ``` +##### 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`. @@ -101,6 +160,10 @@ These are not automatically included and setting the relevant option without add ``` +##### 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 @@ -136,7 +199,7 @@ This package is available on Maven central. com.networknt json-schema-validator - 1.2.0 + 1.3.1 ``` @@ -144,13 +207,13 @@ This package is available on Maven central. ```java dependencies { - implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.2.0'); + implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.3.1'); } ``` ### Validating inputs against a schema -The following example demonstrates how inputs is validated against a schema. It comprises the following steps. +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`. @@ -161,7 +224,9 @@ The following example demonstrates how inputs is validated against a schema. It * 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 2012-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. +// This creates a schema factory that will use Draft 2012-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/")) @@ -174,7 +239,8 @@ config.setPathType(PathType.JSON_POINTER); // Note that setting this to true requires including the optional joni dependency // config.setEcma262Validator(true); -// 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. +// 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" @@ -202,7 +268,9 @@ JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag. // This is better for performance and the remote may choose not to service the request // For instance Cloudflare will block requests that have older Java User-Agent strings eg. Java/1. builder.schemaMappers(schemaMappers -> - schemaMappers.mapPrefix("https://json-schema.org", "classpath:").mapPrefix("http://json-schema.org", "classpath:")) + schemaMappers + .mapPrefix("https://json-schema.org", "classpath:") + .mapPrefix("http://json-schema.org", "classpath:")) ); SchemaValidatorsConfig config = new SchemaValidatorsConfig(); @@ -228,9 +296,185 @@ Set assertions = schema.validate(input, InputFormat.JSON, exe executionContext.getConfig().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. +| Instance Node | The `JsonNode` pointed to by the Instance Location. +| 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 + + +#### 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 = new SchemaValidatorsConfig(); +config.setPathType(PathType.JSON_POINTER); +config.setFormatAssertionsEnabled(true); +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().setAnnotationCollectionPredicate(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` +| `annotationCollectionPredicate`| 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` + +### Schema Validators Configuration + +| Name | Description | Default Value +|--------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------- +| `pathType` | The path type to use for reporting the instance location and evaluation path. Set to `PathType.JSON_POINTER` to use JSON Pointer. | `PathType.DEFAULT` +| `ecma262Validator` | Whether to use the ECMA 262 `joni` library to validate the `pattern` keyword. This requires the dependency to be manually added to the project or a `ClassNotFoundException` will be thrown. | `false` +| `executionContextCustomizer` | This can be used to customize the `ExecutionContext` generated by the `JsonSchema` for each validation run. | `null` +| `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` +| `messageSource` | This is used to retrieve the locale specific messages. | `DefaultMessageSource.getInstance()` +| `locale` | The locale to use for generating messages in the `ValidationMessage`. | `Locale.getDefault()` +| `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` + +## 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 automatically preload all the validators needed and resolve references. At this point, no exceptions will be thrown if a reference cannot be resolved. If there are references that are cyclic, only the first cycle will be preloaded. If you wish to ensure that remote references can all be resolved, the `initializeValidators` method needs to be called on the `JsonSchema` which will throw an exception if there are references that cannot be resolved. + +The `JsonSchema` created from the factory should be cached and reused. Not reusing the `JsonSchema` means that the schema data needs to be repeated parsed with validator instances created and references resolved. + +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. + ## [Quick Start](doc/quickstart.md) +## [Customizing Schema Retrieval](doc/schema-retrieval.md) + ## [Validators](doc/validators.md) ## [Configuration](doc/config.md) @@ -239,8 +483,6 @@ Set assertions = schema.validate(input, InputFormat.JSON, exe ## [YAML Validation](doc/yaml.md) -## [Customizing Schema Retrieval](doc/schema-retrieval.md) - ## [Customized MetaSchema](doc/cust-meta.md) ## [Collector Context](doc/collector-context.md) diff --git a/doc/compatibility.md b/doc/compatibility.md index f20c97e2c..60b0fbb3e 100644 --- a/doc/compatibility.md +++ b/doc/compatibility.md @@ -1,13 +1,20 @@ ## Compatibility with JSON Schema versions -This implementation does not currently generate annotations. - The `pattern` 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. +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). + ### Known Issues -* The `anyOf` applicator currently returns immediately on matching a schema. This results in the `unevaluatedItems` and `unevaluatedProperties` keywords potentially returning an incorrect result as the rest of the schemas in the `anyOf` aren't processed. -* The `unevaluatedItems` keyword does not currently consider `contains`. +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 29 Jan 2024 using version 1.3.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: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%) | ### Legend @@ -79,15 +86,15 @@ The `pattern` validator by default uses the JDK regular expression implementatio #### Content Encoding -Since Draft 2019-09, the `contentEncoding` keyword does not generate assertions. As the implementation currently does not collect annotations this only generates assertions in Draft 7. +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. As the implementation currently does not collect annotations this only generates assertions in Draft 7. +Since Draft 2019-09, the `contentMediaType` keyword does not generate assertions. #### Content Schema -The `contentSchema` keyword does not generate assertions. As the implementation currently does not collect annotations this doesn't do anything. +The `contentSchema` keyword does not generate assertions. #### Pattern @@ -108,7 +115,7 @@ This also requires adding the `joni` dependency. ``` -### Format +#### Format Since Draft 2019-09 the `format` keyword only generates annotations by default and does not generate assertions. @@ -119,7 +126,7 @@ This can be configured on a schema basis by using a meta schema with the appropr | 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 on a per-execution basis by setting the `setFormatAssertionsEnabled` to `true`. +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 | |:----------------------|:-------:|:-------:|:-------:|:-------------:|:-------------:| @@ -143,7 +150,7 @@ This behavior can be overridden to generate assertions on a per-execution basis | uri-template | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 | | uuid | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 | -### Footnotes +##### 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. diff --git a/doc/quickstart.md b/doc/quickstart.md index 65467b817..73c7be412 100644 --- a/doc/quickstart.md +++ b/doc/quickstart.md @@ -1,83 +1,65 @@ ## Quick Start -To use the validator, we need to have both the `JsonSchema` object and `JsonNode` object constructed. -There are many ways to do that. -Here is base test class, that shows several ways to construct these from `String`, `Stream`, `Url`, and `JsonNode`. -Please pay attention to the `JsonSchemaFactory` class as it is the way to construct the `JsonSchema` object. +To use the validator, we need to have the `JsonSchema` loaded and cached. -```java -public class BaseJsonSchemaValidatorTest { +For simplicity the following test loads a schema from a `String` or `JsonNode`. Note that loading a schema in this manner is not recommended as a relative `$ref` will not be properly resolved as there is no base IRI. - private ObjectMapper mapper = new ObjectMapper(); +The preferred method of loading a schema is by using a `SchemaLocation` and by configuring the appropriate `SchemaMapper` and `SchemaLoader` on the `JsonSchemaFactory`. - protected JsonNode getJsonNodeFromClasspath(String name) throws IOException { - InputStream is1 = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(name); - return mapper.readTree(is1); - } +```java +package com.example; - protected JsonNode getJsonNodeFromStringContent(String content) throws IOException { - return mapper.readTree(content); - } +import static org.junit.jupiter.api.Assertions.assertEquals; - protected JsonNode getJsonNodeFromUrl(String url) throws IOException { - return mapper.readTree(new URL(url)); - } +import java.util.Set; - protected JsonSchema getJsonSchemaFromClasspath(String name) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - InputStream is = Thread.currentThread().getContextClassLoader() - .getResourceAsStream(name); - return factory.getSchema(is); - } +import org.junit.jupiter.api.Test; - protected JsonSchema getJsonSchemaFromStringContent(String schemaContent) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(schemaContent); - } +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.*; +import com.networknt.schema.serialization.JsonMapperFactory; - protected JsonSchema getJsonSchemaFromUrl(String uri) throws URISyntaxException { +public class SampleTest { + @Test + void schemaFromString() throws JsonMappingException, JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(SchemaLocation.of(uri)); + /* + * 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],\"enumErrorCode\":\"Not in the list\"}"); + Set errors = schemaFromString.validate("7", InputFormat.JSON); + assertEquals(1, errors.size()); } - protected JsonSchema getJsonSchemaFromJsonNode(JsonNode jsonNode) { + @Test + void schemaFromJsonNode() throws JsonMappingException, JsonProcessingException { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4); - return factory.getSchema(jsonNode); - } - - // Automatically detect version for given JsonNode - protected JsonSchema getJsonSchemaFromJsonNodeAutomaticVersion(JsonNode jsonNode) { - JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersionDetector.detect(jsonNode)); - return factory.getSchema(jsonNode); - } - -} -``` -And the following is one of the test cases in one of the test classes that extend from the above base class. As you can see, it constructs `JsonSchema` and `JsonNode` from `String`. - -```java -class Sample extends BaseJsonSchemaValidatorTest { - - void test() { - JsonSchema schema = getJsonSchemaFromStringContent("{\"enum\":[1, 2, 3, 4],\"enumErrorCode\":\"Not in the list\"}"); - JsonNode node = getJsonNodeFromStringContent("7"); - Set errors = schema.validate(node); - assertThat(errors.size(), is(1)); - - // With automatic version detection - JsonNode schemaNode = getJsonNodeFromStringContent( - "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}"); - JsonSchema schema = getJsonSchemaFromJsonNodeAutomaticVersion(schemaNode); - - schema.initializeValidators(); // by default all schemas are loaded lazily. You can load them eagerly via - // initializeValidators() - - JsonNode node = getJsonNodeFromStringContent("{\"id\": \"2\"}"); - Set errors = schema.validate(node); - assertThat(errors.size(), is(1)); + 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 V4 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); + assertEquals(1, errors.size()); } - } - ``` diff --git a/doc/upgrading.md b/doc/upgrading.md index 0eab6becf..6da6ae54b 100644 --- a/doc/upgrading.md +++ b/doc/upgrading.md @@ -1,7 +1,76 @@ ## Upgrading to new versions +This library can contain breaking changes in `minor` version releases. + This contains information on the notable or breaking changes in each version. +### 1.3.1 + +This does not contain any breaking changes from 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/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` + +This also fixes the issue where the `unevaluatedItems` keyword does not take into account the `contains` keyword when performing the evaluation. + +This also fixes cases where `anyOf` short-circuits to not short-circuit the evaluation if a adjacent `unevaluatedProperties` or `unevaluatedItems` keyword exists. + +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%) | + +#### Performance + +##### NetworkNT 1.3.1 + +``` +Benchmark Mode Cnt Score Error Units +NetworkntBenchmark.testValidate thrpt 10 6776.693 ± 115.309 ops/s +NetworkntBenchmark.testValidate:·gc.alloc.rate thrpt 10 971.191 ± 16.420 MB/sec +NetworkntBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 165318.816 ± 0.459 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 968.894 ± 51.234 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 164933.962 ± 8636.203 B/op +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.002 ± 0.001 MB/sec +NetworkntBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.274 ± 0.218 B/op +NetworkntBenchmark.testValidate:·gc.count thrpt 10 89.000 counts +NetworkntBenchmark.testValidate:·gc.time thrpt 10 99.000 ms +``` + +###### Everit 1.14.1 + +``` +Benchmark Mode Cnt Score Error Units +EveritBenchmark.testValidate thrpt 10 3719.192 ± 125.592 ops/s +EveritBenchmark.testValidate:·gc.alloc.rate thrpt 10 1448.208 ± 74.746 MB/sec +EveritBenchmark.testValidate:·gc.alloc.rate.norm thrpt 10 449621.927 ± 7400.825 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space thrpt 10 1446.397 ± 79.919 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Eden_Space.norm thrpt 10 449159.799 ± 18614.931 B/op +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space thrpt 10 0.001 ± 0.001 MB/sec +EveritBenchmark.testValidate:·gc.churn.G1_Survivor_Space.norm thrpt 10 0.364 ± 0.391 B/op +EveritBenchmark.testValidate:·gc.count thrpt 10 133.000 counts +EveritBenchmark.testValidate:·gc.time thrpt 10 148.000 ms +``` + ### 1.3.0 This adds support for Draft 2020-12 diff --git a/src/main/java/com/networknt/schema/AbstractCollector.java b/src/main/java/com/networknt/schema/AbstractCollector.java index cb0cf4bcc..3a45d81d0 100644 --- a/src/main/java/com/networknt/schema/AbstractCollector.java +++ b/src/main/java/com/networknt/schema/AbstractCollector.java @@ -15,6 +15,11 @@ */ package com.networknt.schema; +/** + * Base collector. + * + * @param the type + */ public abstract class AbstractCollector implements Collector { @Override diff --git a/src/main/java/com/networknt/schema/AbstractJsonValidator.java b/src/main/java/com/networknt/schema/AbstractJsonValidator.java index 75f84623b..b0552a6ff 100644 --- a/src/main/java/com/networknt/schema/AbstractJsonValidator.java +++ b/src/main/java/com/networknt/schema/AbstractJsonValidator.java @@ -16,15 +16,33 @@ package com.networknt.schema; +import java.util.function.Consumer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; + +/** + * Base {@link JsonValidator}. + */ public abstract class AbstractJsonValidator implements JsonValidator { private final SchemaLocation schemaLocation; + private final JsonNode schemaNode; private final JsonNodePath evaluationPath; private final Keyword keyword; - public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword) { + /** + * Constructor. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param keyword the keyword + * @param schemaNode the schema node + */ + public AbstractJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Keyword keyword, JsonNode schemaNode) { this.schemaLocation = schemaLocation; this.evaluationPath = evaluationPath; this.keyword = keyword; + this.schemaNode = schemaNode; } @Override @@ -42,8 +60,52 @@ public String getKeyword() { return keyword.getValue(); } + /** + * The schema node used to create the validator. + * + * @return the schema node + */ + public JsonNode getSchemaNode() { + return this.schemaNode; + } + @Override public String toString() { return getEvaluationPath().getName(-1); } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext) { + return collectAnnotations(executionContext, getKeyword()); + } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @param keyword the keyword + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { + return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() + && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + } + + /** + * Puts an annotation. + * + * @param executionContext the execution context + * @param customizer to customize the annotation + */ + protected void putAnnotation(ExecutionContext executionContext, Consumer customizer) { + JsonNodeAnnotation.Builder builder = JsonNodeAnnotation.builder().evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()); + customizer.accept(builder); + executionContext.getAnnotations().put(builder.build()); + } } diff --git a/src/main/java/com/networknt/schema/AbstractKeyword.java b/src/main/java/com/networknt/schema/AbstractKeyword.java index 4f8feca0a..79878896c 100644 --- a/src/main/java/com/networknt/schema/AbstractKeyword.java +++ b/src/main/java/com/networknt/schema/AbstractKeyword.java @@ -16,16 +16,27 @@ package com.networknt.schema; - +/** + * Abstract keyword. + */ public abstract class AbstractKeyword implements Keyword { private final String value; + /** + * Create abstract keyword. + * + * @param value the keyword + */ public AbstractKeyword(String value) { this.value = value; } + /** + * Gets the keyword. + * + * @return the keyword + */ public String getValue() { return value; } - } diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java index 42994d3b1..ce05c5e66 100644 --- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java @@ -17,6 +17,7 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.regex.RegularExpression; import org.slf4j.Logger; @@ -24,6 +25,9 @@ import java.util.*; +/** + * {@link JsonValidator} for additionalProperties. + */ public class AdditionalPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AdditionalPropertiesValidator.class); @@ -32,6 +36,8 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator { private final Set allowedProperties; private final List patternProperties = new ArrayList<>(); + private Boolean hasUnevaluatedPropertiesValidator; + public AdditionalPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ADDITIONAL_PROPERTIES, validationContext); @@ -71,13 +77,17 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } - CollectorContext collectorContext = executionContext.getCollectorContext(); - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - // if allowAdditionalProperties is true, add all the properties as evaluated. - if (allowAdditionalProperties) { - for (Iterator it = node.fieldNames(); it.hasNext(); ) { - collectorContext.getEvaluatedProperties().add(instanceLocation.append(it.next())); + Set matchedInstancePropertyNames = null; + + boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); + // if allowAdditionalProperties is true, add all the properties as evaluated. + if (allowAdditionalProperties && collectAnnotations) { + for (Iterator it = node.fieldNames(); it.hasNext();) { + if (matchedInstancePropertyNames == null) { + matchedInstancePropertyNames = new LinkedHashSet<>(); } + String fieldName = it.next(); + matchedInstancePropertyNames.add(fieldName); } } @@ -102,8 +112,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().property(pname).instanceLocation(instanceLocation.append(pname)) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(pname).build()); + errors.add(message().instanceNode(node).property(pname) + .instanceLocation(instanceLocation.append(pname)) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(pname).build()); } else { if (additionalPropertiesSchema != null) { ValidatorState state = executionContext.getValidatorState(); @@ -128,6 +140,12 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + if (collectAnnotations) { + executionContext.getAnnotations().put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation).keyword(getKeyword()) + .value(matchedInstancePropertyNames != null ? matchedInstancePropertyNames : Collections.emptySet()) + .build()); + } return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @@ -171,10 +189,22 @@ public Set walk(ExecutionContext executionContext, JsonNode n return Collections.emptySet(); } + private boolean collectAnnotations() { + return hasUnevaluatedPropertiesValidator(); + } + + private boolean hasUnevaluatedPropertiesValidator() { + if (this.hasUnevaluatedPropertiesValidator == null) { + this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties"); + } + return hasUnevaluatedPropertiesValidator; + } + @Override public void preloadJsonSchema() { - if(additionalPropertiesSchema!=null) { + if(additionalPropertiesSchema != null) { additionalPropertiesSchema.initializeValidators(); } + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java index cf1dc2bc0..8442b47d6 100644 --- a/src/main/java/com/networknt/schema/AllOfValidator.java +++ b/src/main/java/com/networknt/schema/AllOfValidator.java @@ -20,11 +20,13 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * {@link JsonValidator} for allOf. + */ public class AllOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AllOfValidator.class); @@ -42,7 +44,6 @@ public AllOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); @@ -50,54 +51,43 @@ public Set validate(ExecutionContext executionContext, JsonNo Set childSchemaErrors = new LinkedHashSet<>(); for (JsonSchema schema : this.schemas) { - Set localErrors = new HashSet<>(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - if (!state.isWalkEnabled()) { - localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); - } else { - localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); - } + Set localErrors = null; - childSchemaErrors.addAll(localErrors); - - if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - final Iterator arrayElements = this.schemaNode.elements(); - while (arrayElements.hasNext()) { - final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); - final JsonNode $ref = allOfEntry.get("$ref"); - if (null != $ref) { - final DiscriminatorContext currentDiscriminatorContext = executionContext - .getCurrentDiscriminatorContext(); - if (null != currentDiscriminatorContext) { - final ObjectNode discriminator = currentDiscriminatorContext - .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); - if (null != discriminator) { - registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, this.parentSchema, instanceLocation); - // now we have to check whether we have hit the right target - final String discriminatorPropertyName = discriminator.get("propertyName").asText(); - final JsonNode discriminatorNode = node.get(discriminatorPropertyName); - final String discriminatorPropertyValue = discriminatorNode == null - ? null - : discriminatorNode.textValue(); - - final JsonSchema jsonSchema = this.parentSchema; - checkDiscriminatorMatch( - currentDiscriminatorContext, - discriminator, - discriminatorPropertyValue, - jsonSchema); - } + if (!state.isWalkEnabled()) { + localErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + } else { + localErrors = schema.walk(executionContext, node, rootNode, instanceLocation, true); + } + + childSchemaErrors.addAll(localErrors); + + if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { + final Iterator arrayElements = this.schemaNode.elements(); + while (arrayElements.hasNext()) { + final ObjectNode allOfEntry = (ObjectNode) arrayElements.next(); + final JsonNode $ref = allOfEntry.get("$ref"); + if (null != $ref) { + final DiscriminatorContext currentDiscriminatorContext = executionContext + .getCurrentDiscriminatorContext(); + if (null != currentDiscriminatorContext) { + final ObjectNode discriminator = currentDiscriminatorContext + .getDiscriminatorForPath(allOfEntry.get("$ref").asText()); + if (null != discriminator) { + registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, + this.parentSchema, instanceLocation); + // now we have to check whether we have hit the right target + final String discriminatorPropertyName = discriminator.get("propertyName").asText(); + final JsonNode discriminatorNode = node.get(discriminatorPropertyName); + final String discriminatorPropertyValue = discriminatorNode == null ? null + : discriminatorNode.textValue(); + + final JsonSchema jsonSchema = this.parentSchema; + checkDiscriminatorMatch(currentDiscriminatorContext, discriminator, + discriminatorPropertyValue, jsonSchema); } } } } - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (localErrors.isEmpty()) { - parentScope.mergeWith(scope); - } } } diff --git a/src/main/java/com/networknt/schema/Annotations.java b/src/main/java/com/networknt/schema/Annotations.java deleted file mode 100644 index 55a0386ca..000000000 --- a/src/main/java/com/networknt/schema/Annotations.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2023 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; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.function.Predicate; - -/** - * Annotations. - */ -public class Annotations { - public static final Set UNEVALUATED_PROPERTIES_ANNOTATIONS; - public static final Set UNEVALUATED_ITEMS_ANNOTATIONS; - public static final Set EVALUATION_ANNOTATIONS; - - public static final Predicate UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; - public static final Predicate UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; - public static final Predicate EVALUATION_ANNOTATIONS_PREDICATE; - public static final Predicate PREDICATE_FALSE; - - static { - Set unevaluatedProperties = new HashSet<>(); - unevaluatedProperties.add("unevaluatedProperties"); - unevaluatedProperties.add("properties"); - unevaluatedProperties.add("patternProperties"); - unevaluatedProperties.add("additionalProperties"); - UNEVALUATED_PROPERTIES_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedProperties); - - Set unevaluatedItems = new HashSet<>(); - unevaluatedItems.add("unevaluatedItems"); - unevaluatedItems.add("items"); - unevaluatedItems.add("prefixItems"); - unevaluatedItems.add("additionalItems"); - unevaluatedItems.add("contains"); - UNEVALUATED_ITEMS_ANNOTATIONS = Collections.unmodifiableSet(unevaluatedItems); - - Set evaluation = new HashSet<>(); - evaluation.addAll(unevaluatedProperties); - evaluation.addAll(unevaluatedItems); - EVALUATION_ANNOTATIONS = Collections.unmodifiableSet(evaluation); - - UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE = UNEVALUATED_PROPERTIES_ANNOTATIONS::contains; - UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE = UNEVALUATED_ITEMS_ANNOTATIONS::contains; - EVALUATION_ANNOTATIONS_PREDICATE = EVALUATION_ANNOTATIONS::contains; - PREDICATE_FALSE = (keyword) -> false; - } - - /** - * Gets the default annotation allow list. - * - * @param metaSchema the meta schema - * @return the default annotation allow set - */ - public static Set getDefaultAnnotationAllowList(JsonMetaSchema metaSchema) { - boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; - boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; - if (unevaluatedProperties && unevaluatedItems) { - return EVALUATION_ANNOTATIONS; - } else if (unevaluatedProperties && !unevaluatedItems) { - return UNEVALUATED_PROPERTIES_ANNOTATIONS; - } else if (!unevaluatedProperties && unevaluatedItems) { - return UNEVALUATED_ITEMS_ANNOTATIONS; - } - return Collections.emptySet(); - } - - /** - * Gets the default annotation allow list predicate. - * - * @param metaSchema the meta schema - * @return the default annotation allow list predicate - */ - public static Predicate getDefaultAnnotationAllowListPredicate(JsonMetaSchema metaSchema) { - boolean unevaluatedProperties = metaSchema.getKeywords().get("unevaluatedProperties") != null; - boolean unevaluatedItems = metaSchema.getKeywords().get("unevaluatedItems") != null; - if (unevaluatedProperties && unevaluatedItems) { - return EVALUATION_ANNOTATIONS_PREDICATE; - } else if (unevaluatedProperties && !unevaluatedItems) { - return UNEVALUATED_PROPERTIES_ANNOTATIONS_PREDICATE; - } else if (!unevaluatedProperties && unevaluatedItems) { - return UNEVALUATED_ITEMS_ANNOTATIONS_PREDICATE; - } - return PREDICATE_FALSE; - } -} diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index df735200f..f99cb7925 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -17,7 +17,6 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,6 +24,9 @@ import java.util.*; import java.util.stream.Collectors; +/** + * {@link JsonValidator} for anyOf. + */ public class AnyOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(AnyOfValidator.class); private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation"; @@ -32,6 +34,8 @@ public class AnyOfValidator extends BaseJsonValidator { private final List schemas = new ArrayList<>(); private final DiscriminatorContext discriminatorContext; + private Boolean canShortCircuit = null; + public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext); int size = schemaNode.size(); @@ -50,7 +54,6 @@ public AnyOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); @@ -63,22 +66,23 @@ public Set validate(ExecutionContext executionContext, JsonNo Set allErrors = new LinkedHashSet<>(); - Scope grandParentScope = collectorContext.enterDynamicScope(); + int numberOfValidSubSchemas = 0; try { - int numberOfValidSubSchemas = 0; - for (JsonSchema schema: this.schemas) { - Set errors = Collections.emptySet(); - Scope parentScope = collectorContext.enterDynamicScope(); - try { + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + for (JsonSchema schema : this.schemas) { + Set errors = Collections.emptySet(); state.setMatchedNode(initialHasMatchedNode); TypeValidator typeValidator = schema.getTypeValidator(); if (typeValidator != null) { - //If schema has type validator and node type doesn't match with schemaType then ignore it - //For union type, it is a must to call TypeValidator + // If schema has type validator and node type doesn't match with schemaType then + // ignore it + // For union type, it is a must to call TypeValidator if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) { - allErrors - .addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); + allErrors.addAll(typeValidator.validate(executionContext, node, rootNode, instanceLocation)); continue; } } @@ -90,7 +94,8 @@ public Set validate(ExecutionContext executionContext, JsonNo // check if any validation errors have occurred if (errors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator + // check whether there are no errors HOWEVER we have validated the exact + // validator if (!state.hasMatchedNode()) { continue; } @@ -98,7 +103,8 @@ public Set validate(ExecutionContext executionContext, JsonNo numberOfValidSubSchemas++; } - if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators())) { + if (errors.isEmpty() && (!this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) + && canShortCircuit()) { // Clear all errors. allErrors.clear(); // return empty errors. @@ -107,8 +113,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.discriminatorContext.isDiscriminatorMatchFound()) { if (!errors.isEmpty()) { allErrors.addAll(errors); - allErrors.add(message().instanceLocation(instanceLocation) + allErrors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(DISCRIMINATOR_REMARK).build()); } else { // Clear all errors. @@ -118,12 +125,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } } allErrors.addAll(errors); - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } // determine only those errors which are NOT of type "required" property missing @@ -138,7 +143,7 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && this.discriminatorContext.isActive()) { final Set errors = new LinkedHashSet<>(); - errors.add(message().instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments( "based on the provided discriminator. No alternative could be chosen based on the discriminator property") @@ -149,13 +154,13 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { executionContext.leaveDiscriminatorContextImmediately(instanceLocation); } - - Scope parentScope = collectorContext.exitDynamicScope(); if (allErrors.isEmpty()) { state.setMatchedNode(true); - grandParentScope.mergeWith(parentScope); } } + if (numberOfValidSubSchemas >= 1) { + return Collections.emptySet(); + } return Collections.unmodifiableSet(allErrors); } @@ -169,9 +174,24 @@ public Set walk(ExecutionContext executionContext, JsonNode n } return new LinkedHashSet<>(); } + + protected boolean canShortCircuit() { + if (this.canShortCircuit == null) { + boolean canShortCircuit = true; + for (JsonValidator validator : getEvaluationParentSchema().getValidators()) { + if ("unevaluatedProperties".equals(validator.getKeyword()) + || "unevaluatedItems".equals(validator.getKeyword())) { + canShortCircuit = false; + } + } + this.canShortCircuit = canShortCircuit; + } + return this.canShortCircuit; + } @Override public void preloadJsonSchema() { preloadJsonSchemas(this.schemas); + canShortCircuit(); // cache flag } } \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 8a30144fa..8419ea3b3 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.i18n.DefaultMessageSource; import org.slf4j.Logger; @@ -26,6 +27,7 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; public abstract class BaseJsonValidator extends ValidationMessageHandler implements JsonValidator { protected final boolean suppressSubSchemaRetrieval; @@ -45,16 +47,15 @@ public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP public BaseJsonValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ErrorMessageType errorMessageType, Keyword keyword, ValidationContext validationContext, boolean suppressSubSchemaRetrieval) { - super(validationContext != null - && validationContext.getConfig() != null && validationContext.getConfig().isFailFast(), - errorMessageType, + super(errorMessageType, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().isCustomMessageSupported() : true, (validationContext != null && validationContext.getConfig() != null) ? validationContext.getConfig().getMessageSource() : DefaultMessageSource.getInstance(), - keyword, parentSchema, schemaLocation, evaluationPath); + keyword, + parentSchema, schemaLocation, evaluationPath); this.validationContext = validationContext; this.schemaNode = schemaNode; this.suppressSubSchemaRetrieval = suppressSubSchemaRetrieval; @@ -286,39 +287,6 @@ public Set validate(ExecutionContext executionContext, JsonNo return validate(executionContext, node, node, atRoot()); } - /** - * Validates to a format. - * - * @param the result type - * @param executionContext the execution context - * @param node the node - * @param format the format - * @return the result - */ - public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format) { - return validate(executionContext, node, format, null); - } - - /** - * Validates to a format. - * - * @param the result type - * @param executionContext the execution context - * @param node the node - * @param format the format - * @param executionCustomizer the customizer - * @return the result - */ - public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format, - ExecutionContextCustomizer executionCustomizer) { - format.customize(executionContext, this.validationContext); - if (executionCustomizer != null) { - executionCustomizer.customize(executionContext, validationContext); - } - Set validationMessages = validate(executionContext, node); - return format.format(validationMessages, executionContext, this.validationContext); - } - protected String getNodeFieldType() { JsonNode typeField = this.getParentSchema().getSchemaNode().get("type"); if (typeField != null) { @@ -346,4 +314,65 @@ protected JsonNodePath atRoot() { public String toString() { return getEvaluationPath().getName(-1); } + + protected boolean hasAdjacentKeywordInEvaluationPath(String keyword) { + boolean hasValidator = validationContext.getMetaSchema().getKeywords() + .get(keyword) != null; + if (hasValidator) { + JsonSchema schema = getEvaluationParentSchema(); + while (schema != null) { + for (JsonValidator validator : schema.getValidators()) { + if (keyword.equals(validator.getKeyword())) { + hasValidator = true; + break; + } + } + if (hasValidator) { + break; + } + schema = schema.getEvaluationParentSchema(); + } + } + return hasValidator; + } + + @Override + protected MessageSourceValidationMessage.Builder message() { + return super.message().schemaNode(this.schemaNode); + } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext) { + return collectAnnotations(executionContext, getKeyword()); + } + + /** + * Determine if annotations should be reported. + * + * @param executionContext the execution context + * @param keyword the keyword + * @return true if annotations should be reported + */ + protected boolean collectAnnotations(ExecutionContext executionContext, String keyword) { + return executionContext.getExecutionConfig().isAnnotationCollectionEnabled() + && executionContext.getExecutionConfig().getAnnotationCollectionPredicate().test(keyword); + } + + /** + * Puts an annotation. + * + * @param executionContext the execution context + * @param customizer to customize the annotation + */ + protected void putAnnotation(ExecutionContext executionContext, Consumer customizer) { + JsonNodeAnnotation.Builder builder = JsonNodeAnnotation.builder().evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()); + customizer.accept(builder); + executionContext.getAnnotations().put(builder.build()); + } } diff --git a/src/main/java/com/networknt/schema/CollectorContext.java b/src/main/java/com/networknt/schema/CollectorContext.java index 07b0c6800..2469fceda 100644 --- a/src/main/java/com/networknt/schema/CollectorContext.java +++ b/src/main/java/com/networknt/schema/CollectorContext.java @@ -15,14 +15,7 @@ */ package com.networknt.schema; -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -42,95 +35,9 @@ public class CollectorContext { */ private Map collectorLoadMap = new HashMap<>(); - private final Deque dynamicScopes = new LinkedList<>(); - private final boolean disableUnevaluatedItems; - private final boolean disableUnevaluatedProperties; - public CollectorContext() { - this(false, false); - } - - public CollectorContext(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties) { - this.disableUnevaluatedItems = disableUnevaluatedItems; - this.disableUnevaluatedProperties = disableUnevaluatedProperties; - this.dynamicScopes.push(newTopScope()); - } - - /** - * Creates a new scope - * @return the previous, parent scope - */ - public Scope enterDynamicScope() { - return enterDynamicScope(null); } - /** - * Creates a new scope - * - * @param containingSchema the containing schema - * @return the previous, parent scope - */ - public Scope enterDynamicScope(JsonSchema containingSchema) { - Scope parent = this.dynamicScopes.peek(); - this.dynamicScopes.push(newScope(null != containingSchema ? containingSchema : parent.getContainingSchema())); - return parent; - } - - /** - * Restores the previous, parent scope - * @return the exited scope - */ - public Scope exitDynamicScope() { - return this.dynamicScopes.pop(); - } - - /** - * Provides the currently active scope - * @return the active scope - */ - public Scope getDynamicScope() { - return this.dynamicScopes.peek(); - } - - public JsonSchema getOutermostSchema() { - - JsonSchema context = getDynamicScope().getContainingSchema(); - if (null == context) { - throw new IllegalStateException("Missing a root schema in the dynamic scope."); - } - - JsonSchema lexicalRoot = context.findLexicalRoot(); - if (lexicalRoot.isRecursiveAnchor()) { - Iterator it = this.dynamicScopes.descendingIterator(); - while (it.hasNext()) { - Scope scope = it.next(); - JsonSchema containingSchema = scope.getContainingSchema(); - if (null != containingSchema && containingSchema.isRecursiveAnchor()) { - return containingSchema; - } - } - } - - return context.findLexicalRoot(); - } - - /** - * Identifies which array items have been evaluated. - * - * @return the set of evaluated items (never null) - */ - public Collection getEvaluatedItems() { - return getDynamicScope().getEvaluatedItems(); - } - - /** - * Identifies which properties have been evaluated. - * - * @return the set of evaluated properties (never null) - */ - public Collection getEvaluatedProperties() { - return getDynamicScope().getEvaluatedProperties(); - } /** * Adds a collector with give name. Preserving this method for backward @@ -222,119 +129,5 @@ void loadCollectors() { this.collectorLoadMap.put(entry.getKey(), collector.collect()); } } - - } - - private Scope newScope(JsonSchema containingSchema) { - return new Scope(this.disableUnevaluatedItems, this.disableUnevaluatedProperties, containingSchema); - } - - private Scope newTopScope() { - return new Scope(true, this.disableUnevaluatedItems, this.disableUnevaluatedProperties, null); - } - - public static class Scope { - - private final JsonSchema containingSchema; - - /** - * Used to track which array items have been evaluated. - */ - private final Collection evaluatedItems; - - /** - * Used to track which properties have been evaluated. - */ - private final Collection evaluatedProperties; - - private final boolean top; - - Scope(boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties, JsonSchema containingSchema) { - this(false, disableUnevaluatedItems, disableUnevaluatedProperties, containingSchema); - } - - Scope(boolean top, boolean disableUnevaluatedItems, boolean disableUnevaluatedProperties, JsonSchema containingSchema) { - this.top = top; - this.containingSchema = containingSchema; - this.evaluatedItems = newCollection(disableUnevaluatedItems); - this.evaluatedProperties = newCollection(disableUnevaluatedProperties); - } - - private static Collection newCollection(boolean disabled) { - return !disabled ? new ArrayList<>() : new AbstractCollection() { - - @Override - public boolean add(JsonNodePath e) { - return false; - } - - @Override - public Iterator iterator() { - return Collections.emptyIterator(); - } - - @Override - public boolean remove(Object o) { - return false; - } - - @Override - public int size() { - return 0; - } - - }; - } - - public boolean isTop() { - return this.top; - } - - public JsonSchema getContainingSchema() { - return this.containingSchema; - } - - /** - * Identifies which array items have been evaluated. - * - * @return the set of evaluated items (never null) - */ - public Collection getEvaluatedItems() { - return this.evaluatedItems; - } - - /** - * Identifies which properties have been evaluated. - * - * @return the set of evaluated properties (never null) - */ - public Collection getEvaluatedProperties() { - return this.evaluatedProperties; - } - - /** - * Merges the provided scope into this scope. - * @param scope the scope to merge - * @return this scope - */ - public Scope mergeWith(Scope scope) { - if (!scope.getEvaluatedItems().isEmpty()) { - getEvaluatedItems().addAll(scope.getEvaluatedItems()); - } - if (!scope.getEvaluatedProperties().isEmpty()) { - getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); - } - return this; - } - - @Override - public String toString() { - return new StringBuilder("{ ") - .append("\"evaluatedItems\": ").append(this.evaluatedItems) - .append(", ") - .append("\"evaluatedProperties\": ").append(this.evaluatedProperties) - .append(" }").toString(); - } - } } diff --git a/src/main/java/com/networknt/schema/ConstValidator.java b/src/main/java/com/networknt/schema/ConstValidator.java index 7c50dbab3..8c8151f24 100644 --- a/src/main/java/com/networknt/schema/ConstValidator.java +++ b/src/main/java/com/networknt/schema/ConstValidator.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for const. + */ public class ConstValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(ConstValidator.class); JsonNode schemaNode; @@ -36,12 +39,13 @@ public Set validate(ExecutionContext executionContext, JsonNo if (schemaNode.isNumber() && node.isNumber()) { if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(schemaNode.asText()) .build()); } } else if (!schemaNode.equals(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()).arguments(schemaNode.asText()).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java index fb0268187..00c177e08 100644 --- a/src/main/java/com/networknt/schema/ContainsValidator.java +++ b/src/main/java/com/networknt/schema/ContainsValidator.java @@ -17,29 +17,35 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Collection; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; import static com.networknt.schema.VersionCode.MinV201909; +/** + * {@link JsonValidator} for contains. + */ public class ContainsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ContainsValidator.class); private static final String CONTAINS_MAX = "contains.max"; private static final String CONTAINS_MIN = "contains.min"; - private static final VersionFlag DEFAULT_VERSION = VersionFlag.V6; private final JsonSchema schema; private final boolean isMinV201909; - private int min = 1; - private int max = Integer.MAX_VALUE; + private Integer min = null; + private Integer max = null; + + private Boolean hasUnevaluatedItemsValidator = null; public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext); @@ -47,7 +53,7 @@ public ContainsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP // Draft 6 added the contains keyword but maxContains and minContains first // appeared in Draft 2019-09 so the semantics of the validation changes // slightly. - isMinV201909 = MinV201909.getVersions().contains(SpecVersionDetector.detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION)); + this.isMinV201909 = MinV201909.getVersions().contains(this.validationContext.getMetaSchema().getSpecification()); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); @@ -69,49 +75,131 @@ public Set validate(ExecutionContext executionContext, JsonNo debug(logger, node, rootNode, instanceLocation); // ignores non-arrays + Set results = null; + int actual = 0, i = 0; + List indexes = new ArrayList<>(); // for the annotation if (null != this.schema && node.isArray()) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); - - int actual = 0, i = 0; - for (JsonNode n : node) { - JsonNodePath path = instanceLocation.append(i); - - if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { - ++actual; - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + for (JsonNode n : node) { + JsonNodePath path = instanceLocation.append(i); + if (this.schema.validate(executionContext, n, rootNode, path).isEmpty()) { + ++actual; + indexes.add(i); } + ++i; } - ++i; + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); + } + int m = 1; // default to 1 if "min" not specified + if (this.min != null) { + m = this.min; + } + if (actual < m) { + results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MIN_CONTAINS : ValidatorTypeCode.CONTAINS, + executionContext.getExecutionConfig().getLocale(), + executionContext.getExecutionConfig().isFailFast(), node, instanceLocation, m); } - if (actual < this.min) { - if(isMinV201909) { - updateValidatorType(ValidatorTypeCode.MIN_CONTAINS); + if (this.max != null && actual > this.max) { + results = boundsViolated(isMinV201909 ? ValidatorTypeCode.MAX_CONTAINS : ValidatorTypeCode.CONTAINS, + executionContext.getExecutionConfig().getLocale(), + executionContext.getExecutionConfig().isFailFast(), node, instanceLocation, this.max); + } + } + + boolean collectAnnotations = collectAnnotations(); + if (this.schema != null) { + // This keyword produces an annotation value which is an array of the indexes to + // which this keyword validates successfully when applying its subschema, in + // ascending order. The value MAY be a boolean "true" if the subschema validates + // successfully when applied to every index of the instance. The annotation MUST + // be present if the instance array to which this keyword's schema applies is + // empty. + + if (collectAnnotations || collectAnnotations(executionContext, "contains")) { + if (actual == i) { + // evaluated all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("contains").value(true).build()); + } else { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("contains").value(indexes).build()); } - return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), - executionContext.getExecutionConfig().getLocale(), instanceLocation, this.min); } - - if (actual > this.max) { - if(isMinV201909) { - updateValidatorType(ValidatorTypeCode.MAX_CONTAINS); + + // Add minContains and maxContains annotations + if (this.min != null) { + String minContainsKeyword = "minContains"; + if (collectAnnotations || collectAnnotations(executionContext, minContainsKeyword)) { + // Omitted keywords MUST NOT produce annotation results. However, as described + // in the section for contains, the absence of this keyword's annotation causes + // contains to assume a minimum value of 1. + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(minContainsKeyword)) + .schemaLocation(this.schemaLocation.append(minContainsKeyword)) + .keyword(minContainsKeyword).value(this.min).build()); + } + } + + if (this.max != null) { + String maxContainsKeyword = "maxContains"; + if (collectAnnotations || collectAnnotations(executionContext, maxContainsKeyword)) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath.append(maxContainsKeyword)) + .schemaLocation(this.schemaLocation.append(maxContainsKeyword)) + .keyword(maxContainsKeyword).value(this.max).build()); } - return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), - executionContext.getExecutionConfig().getLocale(), instanceLocation, this.max); } } - - return Collections.emptySet(); + return results == null ? Collections.emptySet() : results; } @Override public void preloadJsonSchema() { Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators); + collectAnnotations(); // cache the flag + } + + private Set boundsViolated(ValidatorTypeCode validatorTypeCode, Locale locale, boolean failFast, + JsonNode instanceNode, JsonNodePath instanceLocation, int bounds) { + String messageKey = "contains"; + if (ValidatorTypeCode.MIN_CONTAINS.equals(validatorTypeCode)) { + messageKey = CONTAINS_MIN; + } else if (ValidatorTypeCode.MAX_CONTAINS.equals(validatorTypeCode)) { + messageKey = CONTAINS_MAX; + } + return Collections + .singleton(message().instanceNode(instanceNode).instanceLocation(instanceLocation).messageKey(messageKey) + .locale(locale).failFast(failFast).arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()) + .code(validatorTypeCode.getErrorCode()).type(validatorTypeCode.getValue()).build()); + } + + /** + * Determine if annotations must be collected for evaluation. + *

+ * This will be collected regardless of whether it is needed for reporting. + * + * @return true if annotations must be collected for evaluation. + */ + private boolean collectAnnotations() { + return hasUnevaluatedItemsValidator(); } - private Set boundsViolated(String messageKey, Locale locale, JsonNodePath instanceLocation, int bounds) { - return Collections.singleton(message().instanceLocation(instanceLocation).messageKey(messageKey).locale(locale) - .arguments(String.valueOf(bounds), this.schema.getSchemaNode().toString()).build()); + private boolean hasUnevaluatedItemsValidator() { + if (this.hasUnevaluatedItemsValidator == null) { + this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems"); + } + return hasUnevaluatedItemsValidator; } } diff --git a/src/main/java/com/networknt/schema/ContentEncodingValidator.java b/src/main/java/com/networknt/schema/ContentEncodingValidator.java index 579e67921..03a51c31f 100644 --- a/src/main/java/com/networknt/schema/ContentEncodingValidator.java +++ b/src/main/java/com/networknt/schema/ContentEncodingValidator.java @@ -17,15 +17,15 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Base64; import java.util.Collections; import java.util.Set; - /** - * Validation for contentEncoding keyword. + * {@link JsonValidator} for contentEncoding. *

* Note that since 2019-09 this keyword only generates annotations and not * assertions. @@ -34,6 +34,15 @@ public class ContentEncodingValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ContentEncodingValidator.class); private String contentEncoding; + /** + * Constructor. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param schemaNode the schema node + * @param parentSchema the parent schema + * @param validationContext the validation context + */ public ContentEncodingValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_ENCODING, @@ -64,10 +73,17 @@ public Set validate(ExecutionContext executionContext, JsonNo if (nodeType != JsonType.STRING) { return Collections.emptySet(); } + + if (collectAnnotations(executionContext)) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(this.contentEncoding)); + } if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentEncoding).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.contentEncoding) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java index 38bf8780b..7eecf1c3a 100644 --- a/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java +++ b/src/main/java/com/networknt/schema/ContentMediaTypeValidator.java @@ -30,7 +30,7 @@ import com.networknt.schema.serialization.JsonMapperFactory; /** - * Validation for contentMediaType keyword. + * {@link JsonValidator} for contentMediaType. *

* Note that since 2019-09 this keyword only generates annotations and not assertions. */ @@ -40,6 +40,15 @@ public class ContentMediaTypeValidator extends BaseJsonValidator { private static final Pattern PATTERN = Pattern.compile(PATTERN_STRING); private final String contentMediaType; + /** + * Constructor. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param schemaNode the schema node + * @param parentSchema the parent schema + * @param validationContext the validation context + */ public ContentMediaTypeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.CONTENT_MEDIA_TYPE, validationContext); @@ -89,9 +98,16 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } + if (collectAnnotations(executionContext)) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(this.contentMediaType)); + } + if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.contentMediaType).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.contentMediaType) + .build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java index 1415ab9f3..c36789511 100644 --- a/src/main/java/com/networknt/schema/DependenciesValidator.java +++ b/src/main/java/com/networknt/schema/DependenciesValidator.java @@ -22,11 +22,23 @@ import java.util.*; +/** + * {@link JsonValidator} for dependencies. + */ public class DependenciesValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(DependenciesValidator.class); private final Map> propertyDeps = new HashMap>(); private final Map schemaDeps = new HashMap(); + /** + * Constructor. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param schemaNode the schema node + * @param parentSchema the parent schema + * @param validationContext the validation context + */ public DependenciesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DEPENDENCIES, validationContext); @@ -61,8 +73,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (deps != null && !deps.isEmpty()) { for (String field : deps) { if (node.get(field) == null) { - errors.add(message().property(pname).instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(propertyDeps.toString()).build()); } } diff --git a/src/main/java/com/networknt/schema/DependentRequired.java b/src/main/java/com/networknt/schema/DependentRequired.java index 24248be27..dda8ec5ae 100644 --- a/src/main/java/com/networknt/schema/DependentRequired.java +++ b/src/main/java/com/networknt/schema/DependentRequired.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} for dependentRequired. + */ public class DependentRequired extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(DependentRequired.class); private final Map> propertyDependencies = new HashMap>(); @@ -54,8 +57,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (dependencies != null && !dependencies.isEmpty()) { for (String field : dependencies) { if (node.get(field) == null) { - errors.add(message().property(pname).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(field, pname) + errors.add(message().instanceNode(node).property(pname).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(field, pname) .build()); } } diff --git a/src/main/java/com/networknt/schema/DependentSchemas.java b/src/main/java/com/networknt/schema/DependentSchemas.java index 51ee49eac..5781eb54b 100644 --- a/src/main/java/com/networknt/schema/DependentSchemas.java +++ b/src/main/java/com/networknt/schema/DependentSchemas.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} for dependentSchemas. + */ public class DependentSchemas extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DependentSchemas.class); private final Map schemaDependencies = new HashMap<>(); diff --git a/src/main/java/com/networknt/schema/DynamicRefValidator.java b/src/main/java/com/networknt/schema/DynamicRefValidator.java index d2ce7a67a..a113d1167 100644 --- a/src/main/java/com/networknt/schema/DynamicRefValidator.java +++ b/src/main/java/com/networknt/schema/DynamicRefValidator.java @@ -17,14 +17,13 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; /** - * Resolves $dynamicRef. + * {@link JsonValidator} that resolves $dynamicRef. */ public class DynamicRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DynamicRefValidator.class); @@ -32,7 +31,7 @@ public class DynamicRefValidator extends BaseJsonValidator { protected JsonSchemaRef schema; public DynamicRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext); + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.DYNAMIC_REF, validationContext); String refValue = schemaNode.asText(); this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); } @@ -85,72 +84,42 @@ private static String resolve(JsonSchema parentSchema, String refValue) { } return SchemaLocation.resolve(base.getSchemaLocation(), refValue); } - @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = Collections.emptySet(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) - .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) - .arguments(schemaNode.asText()).build(); - throw new JsonSchemaException(validationMessage); - } - errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + debug(logger, node, rootNode, instanceLocation); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.DYNAMIC_REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); } - return errors; + return refSchema.validate(executionContext, node, rootNode, instanceLocation); } @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = Collections.emptySet(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); - // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, - // these schemas will be cached along with config. We have to replace the config for cached $ref references - // with the latest config. Reset the config. - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) - .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) - .arguments(schemaNode.asText()).build(); - throw new JsonSchemaException(validationMessage); - } - errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); - return errors; - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } - } + debug(logger, node, rootNode, instanceLocation); + // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, + // these schemas will be cached along with config. We have to replace the config for cached $ref references + // with the latest config. Reset the config. + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.DYNAMIC_REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); } + return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } public JsonSchemaRef getSchemaRef() { return this.schema; } - @Override public void preloadJsonSchema() { JsonSchema jsonSchema = null; @@ -168,14 +137,14 @@ public void preloadJsonSchema() { SchemaLocation schemaLocation = jsonSchema.getSchemaLocation(); JsonSchema check = jsonSchema; boolean circularDependency = false; - while(check.getEvaluationParentSchema() != null) { + while (check.getEvaluationParentSchema() != null) { check = check.getEvaluationParentSchema(); if (check.getSchemaLocation().equals(schemaLocation)) { circularDependency = true; break; } } - if(!circularDependency) { + if (!circularDependency) { jsonSchema.initializeValidators(); } } diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java index eb006b584..1e088c648 100644 --- a/src/main/java/com/networknt/schema/EnumValidator.java +++ b/src/main/java/com/networknt/schema/EnumValidator.java @@ -28,6 +28,9 @@ import java.util.HashSet; import java.util.Set; +/** + * {@link JsonValidator} for enum. + */ public class EnumValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(EnumValidator.class); @@ -87,8 +90,9 @@ public Set validate(ExecutionContext executionContext, JsonNo node = processArrayNode((ArrayNode) node); } if (!nodes.contains(node) && !( this.validationContext.getConfig().isTypeLoose() && isTypeLooseContainsInEnum(node))) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(error).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(error).build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java index b67894600..88c1bde82 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java @@ -27,6 +27,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for exclusiveMaximum. + */ public class ExclusiveMaximumValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ExclusiveMaximumValidator.class); @@ -103,9 +106,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) - .build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(typedMaximum.thresholdValue()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java index db0655330..a53a56b76 100644 --- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java +++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java @@ -27,6 +27,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for exclusiveMinimum. + */ public class ExclusiveMinimumValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ExclusiveMinimumValidator.class); @@ -110,9 +113,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) - .build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(typedMinimum.thresholdValue()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/ExecutionConfig.java b/src/main/java/com/networknt/schema/ExecutionConfig.java index 594e3e88d..f1a04569b 100644 --- a/src/main/java/com/networknt/schema/ExecutionConfig.java +++ b/src/main/java/com/networknt/schema/ExecutionConfig.java @@ -24,70 +24,53 @@ * Configuration per execution. */ public class ExecutionConfig { + /** + * The locale to use for formatting messages. + */ private Locale locale = Locale.ROOT; - private Predicate annotationAllowedPredicate = (keyword) -> true; + + /** + * Determines if annotation collection is enabled. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + */ + private boolean annotationCollectionEnabled = false; + + /** + * If annotation collection is enabled, determine which annotations to collect. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + */ + private Predicate annotationCollectionPredicate = keyword -> false; /** * Since Draft 2019-09 format assertions are not enabled by default. */ private Boolean formatAssertionsEnabled = null; - public Locale getLocale() { - return locale; - } - - public void setLocale(Locale locale) { - this.locale = Objects.requireNonNull(locale, "Locale must not be null"); - } + /** + * Determine if the validation execution can fail fast. + */ + private boolean failFast = false; /** - * Gets the predicate to determine if annotation collection is allowed for a - * particular keyword. - *

- * The default value is to allow annotation collection. - *

- * Setting this to return false improves performance but keywords such as - * unevaluatedItems and unevaluatedProperties will fail to evaluate properly. - *

- * This will also affect reporting if annotations need to be in the output - * format. - *

- * unevaluatedProperties depends on properties, patternProperties and - * additionalProperties. - *

- * unevaluatedItems depends on items/prefixItems, additionalItems/items and - * contains. + * Gets the locale to use for formatting messages. * - * @return the predicate to determine if annotation collection is allowed for - * the keyword + * @return the locale */ - public Predicate getAnnotationAllowedPredicate() { - return annotationAllowedPredicate; + public Locale getLocale() { + return locale; } /** - * Predicate to determine if annotation collection is allowed for a particular - * keyword. - *

- * The default value is to allow annotation collection. - *

- * Setting this to return false improves performance but keywords such as - * unevaluatedItems and unevaluatedProperties will fail to evaluate properly. - *

- * This will also affect reporting if annotations need to be in the output - * format. - *

- * unevaluatedProperties depends on properties, patternProperties and - * additionalProperties. - *

- * unevaluatedItems depends on items/prefixItems, additionalItems/items and - * contains. + * Sets the locale to use for formatting messages. * - * @param annotationAllowedPredicate the predicate accepting the keyword + * @param locale the locale */ - public void setAnnotationAllowedPredicate(Predicate annotationAllowedPredicate) { - this.annotationAllowedPredicate = Objects.requireNonNull(annotationAllowedPredicate, - "annotationAllowedPredicate must not be null"); + public void setLocale(Locale locale) { + this.locale = Objects.requireNonNull(locale, "Locale must not be null"); } /** @@ -113,4 +96,88 @@ public Boolean getFormatAssertionsEnabled() { public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { this.formatAssertionsEnabled = formatAssertionsEnabled; } + + /** + * Return if fast fail is enabled. + * + * @return if fast fail is enabled + */ + public boolean isFailFast() { + return failFast; + } + + /** + * Sets whether fast fail is enabled. + * + * @param failFast true to fast fail + */ + public void setFailFast(boolean failFast) { + this.failFast = failFast; + } + + /** + * Return if annotation collection is enabled. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + *

+ * The annotations to collect can be customized using the annotation collection + * predicate. + * + * @return if annotation collection is enabled + */ + protected boolean isAnnotationCollectionEnabled() { + return annotationCollectionEnabled; + } + + /** + * Sets whether to annotation collection is enabled. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + *

+ * The annotations to collect can be customized using the annotation collection + * predicate. + * + * @param annotationCollectionEnabled true to enable annotation collection + */ + protected void setAnnotationCollectionEnabled(boolean annotationCollectionEnabled) { + this.annotationCollectionEnabled = annotationCollectionEnabled; + } + + /** + * Gets the predicate to determine if annotation collection is allowed for a + * particular keyword. This only has an effect if annotation collection is + * enabled. + *

+ * The default value is to not collect any annotation keywords if annotation + * collection is enabled. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + * + * @return the predicate to determine if annotation collection is allowed for + * the keyword + */ + public Predicate getAnnotationCollectionPredicate() { + return annotationCollectionPredicate; + } + + /** + * Predicate to determine if annotation collection is allowed for a particular + * keyword. This only has an effect if annotation collection is enabled. + *

+ * The default value is to not collect any annotation keywords if annotation + * collection is enabled. + *

+ * This does not affect annotation collection required for evaluating keywords + * such as unevaluatedItems or unevaluatedProperties and only affects reporting. + * + * @param annotationCollectionPredicate the predicate accepting the keyword + */ + public void setAnnotationCollectionPredicate(Predicate annotationCollectionPredicate) { + this.annotationCollectionPredicate = Objects.requireNonNull(annotationCollectionPredicate, + "annotationCollectionPredicate must not be null"); + } + } diff --git a/src/main/java/com/networknt/schema/ExecutionContext.java b/src/main/java/com/networknt/schema/ExecutionContext.java index c3129eb03..05becdd23 100644 --- a/src/main/java/com/networknt/schema/ExecutionContext.java +++ b/src/main/java/com/networknt/schema/ExecutionContext.java @@ -16,6 +16,9 @@ package com.networknt.schema; +import com.networknt.schema.annotation.JsonNodeAnnotations; +import com.networknt.schema.result.JsonNodeResults; + import java.util.Stack; /** @@ -26,6 +29,8 @@ public class ExecutionContext { private CollectorContext collectorContext; private ValidatorState validatorState = null; private Stack discriminatorContexts = new Stack<>(); + private JsonNodeAnnotations annotations = new JsonNodeAnnotations(); + private JsonNodeResults results = new JsonNodeResults(); /** * Creates an execution context. @@ -99,6 +104,14 @@ public void setExecutionConfig(ExecutionConfig executionConfig) { this.executionConfig = executionConfig; } + public JsonNodeAnnotations getAnnotations() { + return annotations; + } + + public JsonNodeResults getResults() { + return results; + } + /** * Gets the validator state. * diff --git a/src/main/java/com/networknt/schema/FailFastAssertionException.java b/src/main/java/com/networknt/schema/FailFastAssertionException.java new file mode 100644 index 000000000..6ea3cf455 --- /dev/null +++ b/src/main/java/com/networknt/schema/FailFastAssertionException.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2024 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; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +/** + * Thrown when an assertion happens and the evaluation can fail fast. + *

+ * This doesn't extend off JsonSchemaException as it is used for flow control + * and is intended to be caught in a specific place. + *

+ * This will be caught in the JsonSchema validate method to be passed to the + * output formatter. + */ +public class FailFastAssertionException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private final ValidationMessage validationMessage; + + /** + * Constructor. + * + * @param validationMessage the validation message + */ + public FailFastAssertionException(ValidationMessage validationMessage) { + this.validationMessage = Objects.requireNonNull(validationMessage); + } + + /** + * Gets the validation message. + * + * @return the validation message + */ + public ValidationMessage getValidationMessage() { + return this.validationMessage; + } + + /** + * Gets the validation message. + * + * @return the validation message + */ + public Set getValidationMessages() { + return Collections.singleton(this.validationMessage); + } + + @Override + public String getMessage() { + return this.validationMessage != null ? this.validationMessage.getMessage() : super.getMessage(); + } + + @Override + public Throwable fillInStackTrace() { + /* + * This is overridden for performance as filling in the stack trace is expensive + * and this is used for flow control. + */ + return this; + } +} diff --git a/src/main/java/com/networknt/schema/FalseValidator.java b/src/main/java/com/networknt/schema/FalseValidator.java index 675b4a601..3fa2ba4b2 100644 --- a/src/main/java/com/networknt/schema/FalseValidator.java +++ b/src/main/java/com/networknt/schema/FalseValidator.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for false. + */ public class FalseValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(FalseValidator.class); @@ -32,7 +35,8 @@ public FalseValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); // For the false validator, it is always not valid - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } } diff --git a/src/main/java/com/networknt/schema/Format.java b/src/main/java/com/networknt/schema/Format.java index 3120cbcdf..18f80b813 100644 --- a/src/main/java/com/networknt/schema/Format.java +++ b/src/main/java/com/networknt/schema/Format.java @@ -16,12 +16,22 @@ package com.networknt.schema; +/** + * Used to implement the various formats for the format keyword. + */ public interface Format { /** * @return the format name as referred to in a json schema format node. */ String getName(); + /** + * Determines if the value matches the format. + * + * @param executionContext the execution context + * @param value to match + * @return true if matches + */ boolean matches(ExecutionContext executionContext, String value); String getErrorMessageDescription(); diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java index cb3cb1f58..9fa21b313 100644 --- a/src/main/java/com/networknt/schema/FormatValidator.java +++ b/src/main/java/com/networknt/schema/FormatValidator.java @@ -40,6 +40,13 @@ public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPat public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); + if (format != null) { + if (collectAnnotations(executionContext)) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(this.format.getName())); + } + } + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { return Collections.emptySet(); @@ -52,14 +59,15 @@ public Set validate(ExecutionContext executionContext, JsonNo if(!node.textValue().trim().equals(node.textValue())) { if (assertionsEnabled) { // leading and trailing spaces - errors.add(message().instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(format.getName(), format.getErrorMessageDescription()).build()); } } else if(node.textValue().contains("%")) { if (assertionsEnabled) { // zone id is not part of the ipv6 - errors.add(message().instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(format.getName(), format.getErrorMessageDescription()).build()); } @@ -68,7 +76,7 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!format.matches(executionContext, node.textValue())) { if (assertionsEnabled) { - errors.add(message().instanceLocation(instanceLocation) + errors.add(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments(format.getName(), format.getErrorMessageDescription()).build()); } diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java index 0d90912ff..f6bacb795 100644 --- a/src/main/java/com/networknt/schema/IfValidator.java +++ b/src/main/java/com/networknt/schema/IfValidator.java @@ -17,13 +17,15 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} for if. + */ public class IfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(IfValidator.class); @@ -64,38 +66,25 @@ public IfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, J @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); - Set errors = new LinkedHashSet<>(); - Scope parentScope = collectorContext.enterDynamicScope(); boolean ifConditionPassed = false; - try { - try { - ifConditionPassed = this.ifSchema.validate(executionContext, node, rootNode, instanceLocation).isEmpty(); - } catch (JsonSchemaException ex) { - // When failFast is enabled, validations are thrown as exceptions. - // An exception means the condition failed - ifConditionPassed = false; - } - - if (ifConditionPassed && this.thenSchema != null) { - errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, instanceLocation)); - } else if (!ifConditionPassed && this.elseSchema != null) { - // discard ifCondition results - collectorContext.exitDynamicScope(); - collectorContext.enterDynamicScope(); - - errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, instanceLocation)); - } + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + ifConditionPassed = this.ifSchema.validate(executionContext, node, rootNode, instanceLocation).isEmpty(); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } + if (ifConditionPassed && this.thenSchema != null) { + errors.addAll(this.thenSchema.validate(executionContext, node, rootNode, instanceLocation)); + } else if (!ifConditionPassed && this.elseSchema != null) { + errors.addAll(this.elseSchema.validate(executionContext, node, rootNode, instanceLocation)); + } return Collections.unmodifiableSet(errors); } diff --git a/src/main/java/com/networknt/schema/InvalidSchemaException.java b/src/main/java/com/networknt/schema/InvalidSchemaException.java new file mode 100644 index 000000000..e60a34811 --- /dev/null +++ b/src/main/java/com/networknt/schema/InvalidSchemaException.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 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; + +import java.util.Objects; + +/** + * Thrown when an invalid schema is used. + */ +public class InvalidSchemaException extends JsonSchemaException { + private static final long serialVersionUID = 1L; + + public InvalidSchemaException(ValidationMessage message, Exception cause) { + super(Objects.requireNonNull(message)); + this.initCause(cause); + } + + public InvalidSchemaException(ValidationMessage message) { + super(Objects.requireNonNull(message)); + } +} diff --git a/src/main/java/com/networknt/schema/InvalidSchemaRefException.java b/src/main/java/com/networknt/schema/InvalidSchemaRefException.java new file mode 100644 index 000000000..9aad3a5c6 --- /dev/null +++ b/src/main/java/com/networknt/schema/InvalidSchemaRefException.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 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; + +/** + * Thrown when an invalid schema ref is used. + */ +public class InvalidSchemaRefException extends InvalidSchemaException { + private static final long serialVersionUID = 1L; + + public InvalidSchemaRefException(ValidationMessage message, Exception cause) { + super(message, cause); + } + + public InvalidSchemaRefException(ValidationMessage message) { + super(message); + } +} diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java index 822406486..0ba438f11 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator.java +++ b/src/main/java/com/networknt/schema/ItemsValidator.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.walk.DefaultItemWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; @@ -26,6 +27,9 @@ import java.util.*; +/** + * {@link JsonValidator} for items V4 to V2019-09. + */ public class ItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ItemsValidator.class); private static final String PROPERTY_ADDITIONAL_ITEMS = "additionalItems"; @@ -36,6 +40,8 @@ public class ItemsValidator extends BaseJsonValidator { private final JsonSchema additionalSchema; private WalkListenerRunner arrayItemWalkListenerRunner; + private Boolean hasUnevaluatedItemsValidator = null; + public ItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext); @@ -78,72 +84,106 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays return Collections.emptySet(); } + boolean collectAnnotations = collectAnnotations(); + + // Add items annotation + if (collectAnnotations || collectAnnotations(executionContext)) { + if (this.schema != null) { + // Applies to all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } else if (this.tupleSchema != null) { + // Tuples + int items = node.isArray() ? node.size() : 1; + int schemas = this.tupleSchema.size(); + if (items > schemas) { + // More items than schemas so the keyword only applied to the number of schemas + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(schemas).build()); + } else { + // Applies to all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } + } + } + + boolean hasAdditionalItem = false; Set errors = new LinkedHashSet<>(); if (node.isArray()) { int i = 0; for (JsonNode n : node) { - doValidate(executionContext, errors, i, n, rootNode, instanceLocation); + if (doValidate(executionContext, errors, i, n, rootNode, instanceLocation)) { + hasAdditionalItem = true; + } i++; } } else { - doValidate(executionContext, errors, 0, node, rootNode, instanceLocation); + if (doValidate(executionContext, errors, 0, node, rootNode, instanceLocation)) { + hasAdditionalItem = true; + } + } + + if (hasAdditionalItem) { + if (collectAnnotations || collectAnnotations(executionContext, "additionalItems")) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("additionalItems").value(true).build()); + } } return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } - private void doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, + private boolean doValidate(ExecutionContext executionContext, Set errors, int i, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + boolean isAdditionalItem = false; JsonNodePath path = instanceLocation.append(i); if (this.schema != null) { // validate with item schema (the whole array has the same item // schema) Set results = this.schema.validate(executionContext, node, rootNode, path); - if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } - } else { + if (!results.isEmpty()) { errors.addAll(results); } } else if (this.tupleSchema != null) { if (i < this.tupleSchema.size()) { // validate against tuple schema Set results = this.tupleSchema.get(i).validate(executionContext, node, rootNode, path); - if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } - } else { + if (!results.isEmpty()) { errors.addAll(results); } } else { + if ((this.additionalItems != null && this.additionalItems) || this.additionalSchema != null) { + isAdditionalItem = true; + } + if (this.additionalSchema != null) { // validate against additional item schema Set results = this.additionalSchema.validate(executionContext, node, rootNode, path); - if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } - } else { + if (!results.isEmpty()) { errors.addAll(results); } } else if (this.additionalItems != null) { if (this.additionalItems) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } +// evaluatedItems.add(path); } else { // no additional item allowed, return error - errors.add(message().instanceLocation(path) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(i).build()); + errors.add(message().instanceNode(node).instanceLocation(path) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(i).build()); } } } -// } else { -// evaluatedItems.add(path); } + return isAdditionalItem; } @Override @@ -213,6 +253,17 @@ public List getTupleSchema() { public JsonSchema getSchema() { return this.schema; } + + private boolean collectAnnotations() { + return hasUnevaluatedItemsValidator(); + } + + private boolean hasUnevaluatedItemsValidator() { + if (this.hasUnevaluatedItemsValidator == null) { + this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems"); + } + return hasUnevaluatedItemsValidator; + } @Override public void preloadJsonSchema() { @@ -223,5 +274,6 @@ public void preloadJsonSchema() { if (null != this.additionalSchema) { this.additionalSchema.initializeValidators(); } + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/ItemsValidator202012.java b/src/main/java/com/networknt/schema/ItemsValidator202012.java index 6f770a4ff..459867c83 100644 --- a/src/main/java/com/networknt/schema/ItemsValidator202012.java +++ b/src/main/java/com/networknt/schema/ItemsValidator202012.java @@ -18,6 +18,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.walk.DefaultItemWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; @@ -26,6 +27,9 @@ import java.util.*; +/** + * {@link JsonValidator} for items from V2012-12. + */ public class ItemsValidator202012 extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ItemsValidator202012.class); @@ -33,6 +37,8 @@ public class ItemsValidator202012 extends BaseJsonValidator { private final WalkListenerRunner arrayItemWalkListenerRunner; private final int prefixCount; + private Boolean hasUnevaluatedItemsValidator = null; + public ItemsValidator202012(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS_202012, validationContext); @@ -61,18 +67,27 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays if (node.isArray()) { Set errors = new LinkedHashSet<>(); - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); +// Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); + boolean evaluated = false; for (int i = this.prefixCount; i < node.size(); ++i) { JsonNodePath path = instanceLocation.append(i); // validate with item schema (the whole array has the same item schema) Set results = this.schema.validate(executionContext, node.get(i), rootNode, path); if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } +// evaluatedItems.add(path); } else { errors.addAll(results); } + evaluated = true; + } + if (evaluated) { + if (collectAnnotations() || collectAnnotations(executionContext)) { + // Applies to all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } } return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } else { @@ -145,6 +160,18 @@ public JsonSchema getSchema() { @Override public void preloadJsonSchema() { this.schema.initializeValidators(); + collectAnnotations(); // cache the flag + } + + private boolean collectAnnotations() { + return hasUnevaluatedItemsValidator(); + } + + private boolean hasUnevaluatedItemsValidator() { + if (this.hasUnevaluatedItemsValidator == null) { + this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems"); + } + return hasUnevaluatedItemsValidator; } } diff --git a/src/main/java/com/networknt/schema/JsonNodePath.java b/src/main/java/com/networknt/schema/JsonNodePath.java index d7210be90..55001859b 100644 --- a/src/main/java/com/networknt/schema/JsonNodePath.java +++ b/src/main/java/com/networknt/schema/JsonNodePath.java @@ -175,6 +175,29 @@ public boolean startsWith(JsonNodePath other) { } } + /** + * Tests if this path contains a string segment that is an exact match. + *

+ * This will not match if the segment is a number. + * + * @param segment the segment to test + * @return true if the string segment is found + */ + public boolean contains(String segment) { + boolean result = segment.equals(this.pathSegment); + if (result) { + return true; + } + JsonNodePath path = this.getParent(); + while (path != null) { + if (segment.equals(path.pathSegment)) { + return true; + } + path = path.getParent(); + } + return false; + } + @Override public String toString() { if (this.value == null) { diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java index 435b07027..acada7066 100644 --- a/src/main/java/com/networknt/schema/JsonSchema.java +++ b/src/main/java/com/networknt/schema/JsonSchema.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.networknt.schema.CollectorContext.Scope; import com.networknt.schema.SpecVersion.VersionFlag; import com.networknt.schema.serialization.JsonMapperFactory; import com.networknt.schema.serialization.YamlMapperFactory; @@ -62,30 +61,66 @@ static JsonSchema from(ValidationContext validationContext, SchemaLocation schem private boolean hasNoFragment(SchemaLocation schemaLocation) { return this.schemaLocation.getFragment() == null || this.schemaLocation.getFragment().getNameCount() == 0; } + + private static SchemaLocation resolve(SchemaLocation schemaLocation, JsonNode schemaNode, boolean rootSchema, + ValidationContext validationContext) { + String id = validationContext.resolveSchemaId(schemaNode); + if (id != null) { + String resolve = id; + int fragment = id.indexOf('#'); + // Check if there is a non-empty fragment + if (fragment != -1 && !(fragment + 1 >= id.length())) { + // strip the fragment when resolving + resolve = id.substring(0, fragment); + } + SchemaLocation result = !"".equals(resolve) ? schemaLocation.resolve(resolve) : schemaLocation; + JsonSchemaIdValidator validator = validationContext.getConfig().getSchemaIdValidator(); + if (validator != null) { + if (!validator.validate(id, rootSchema, schemaLocation, result, validationContext)) { + SchemaLocation idSchemaLocation = schemaLocation.append(validationContext.getMetaSchema().getIdKeyword()); + ValidationMessage validationMessage = ValidationMessage.builder() + .code(ValidatorTypeCode.ID.getValue()).type(ValidatorTypeCode.ID.getValue()) + .instanceLocation(idSchemaLocation.getFragment()) + .arguments(schemaLocation.toString(), id) + .schemaLocation(idSchemaLocation) + .schemaNode(schemaNode) + .messageFormatter(args -> validationContext.getConfig().getMessageSource().getMessage( + ValidatorTypeCode.ID.getValue(), validationContext.getConfig().getLocale(), args)) + .build(); + throw new InvalidSchemaException(validationMessage); + } + } + return result; + } else { + return schemaLocation; + } + } private JsonSchema(ValidationContext validationContext, SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parent, boolean suppressSubSchemaRetrieval) { - super(schemaLocation.resolve(validationContext.resolveSchemaId(schemaNode)), evaluationPath, schemaNode, parent, + super(resolve(schemaLocation, schemaNode, parent == null, validationContext), evaluationPath, schemaNode, parent, null, null, validationContext, suppressSubSchemaRetrieval); this.metaSchema = this.validationContext.getMetaSchema(); initializeConfig(); String id = this.validationContext.resolveSchemaId(this.schemaNode); if (id != null) { - // In earlier drafts $id may contain an anchor fragment + // In earlier drafts $id may contain an anchor fragment see draft4/idRef.json // Note that json pointer fragments in $id are not allowed - if (hasNoFragment(schemaLocation)) { + SchemaLocation result = id.contains("#") ? schemaLocation.resolve(id) : this.schemaLocation; + if (hasNoFragment(result)) { this.id = id; } else { - this.id = id; + // This is an anchor fragment and is not a document + // This will be added to schema resources later + this.id = null; } - this.validationContext.getSchemaResources() - .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : id, this); + this.validationContext.getSchemaResources().putIfAbsent(result != null ? result.toString() : id, this); } else { if (hasNoFragment(schemaLocation)) { // No $id but there is no fragment and is thus a schema resource - this.id = this.schemaLocation.getAbsoluteIri() != null ? this.schemaLocation.getAbsoluteIri().toString() : ""; + this.id = schemaLocation.getAbsoluteIri() != null ? schemaLocation.getAbsoluteIri().toString() : ""; this.validationContext.getSchemaResources() - .putIfAbsent(this.schemaLocation != null ? this.schemaLocation.toString() : this.id, this); + .putIfAbsent(schemaLocation != null ? schemaLocation.toString() : this.id, this); } else { this.id = null; } @@ -248,6 +283,10 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { JsonSchema document = findSchemaResourceRoot(); JsonSchema parent = document; JsonSchema subSchema = null; + JsonNode parentNode = parent.getSchemaNode(); + SchemaLocation schemaLocation = document.getSchemaLocation(); + JsonNodePath evaluationPath = document.getEvaluationPath(); + int nameCount = fragment.getNameCount(); for (int x = 0; x < fragment.getNameCount(); x++) { /* * The sub schema is created by iterating through the parents in order to @@ -258,10 +297,8 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { * to $id changes in the lexical scope. */ Object segment = fragment.getElement(x); - JsonNode subSchemaNode = parent.getNode(segment); + JsonNode subSchemaNode = getNode(parentNode, segment); if (subSchemaNode != null) { - SchemaLocation schemaLocation = parent.getSchemaLocation(); - JsonNodePath evaluationPath = parent.getEvaluationPath(); if (segment instanceof Number) { int index = ((Number) segment).intValue(); schemaLocation = schemaLocation.append(index); @@ -274,9 +311,17 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { * The parent validation context is used to create as there can be changes in * $schema is later drafts which means the validation context can change. */ - subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode, - parent); - parent = subSchema; + // This may need a redesign see Issue 939 and 940 + String id = parent.getValidationContext().resolveSchemaId(subSchemaNode); +// if (!("definitions".equals(segment.toString()) || "$defs".equals(segment.toString()) +// )) { + if (id != null || x == nameCount - 1) { + subSchema = parent.getValidationContext().newSchema(schemaLocation, evaluationPath, subSchemaNode, + parent); + parent = subSchema; + schemaLocation = subSchema.getSchemaLocation(); + } + parentNode = subSchemaNode; } else { /* * This means that the fragment wasn't found in the document. @@ -290,9 +335,14 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { found = found.getSubSchema(fragment); } if (found == null) { - throw new JsonSchemaException("Unable to find subschema " + fragment.toString() + " in " - + parent.getSchemaLocation().toString() + " at evaluation path " - + parent.getEvaluationPath().toString()); + ValidationMessage validationMessage = ValidationMessage.builder() + .type(ValidatorTypeCode.REF.getValue()).code("internal.unresolvedRef") + .message("{0}: Reference {1} cannot be resolved") + .instanceLocation(schemaLocation.getFragment()) + .schemaLocation(schemaLocation) + .evaluationPath(evaluationPath) + .arguments(fragment).build(); + throw new InvalidSchemaRefException(validationMessage); } return found; } @@ -301,7 +351,10 @@ public JsonSchema getSubSchema(JsonNodePath fragment) { } protected JsonNode getNode(Object propertyOrIndex) { - JsonNode node = getSchemaNode(); + return getNode(this.schemaNode, propertyOrIndex); + } + + protected JsonNode getNode(JsonNode node, Object propertyOrIndex) { JsonNode value = null; if (propertyOrIndex instanceof Number) { value = node.get(((Number) propertyOrIndex).intValue()); @@ -506,36 +559,24 @@ public Set validate(ExecutionContext executionContext, JsonNo SchemaValidatorsConfig config = this.validationContext.getConfig(); Set errors = null; - // Get the collector context. - CollectorContext collectorContext = executionContext.getCollectorContext(); // Set the walkEnabled and isValidationEnabled flag in internal validator state. setValidatorState(executionContext, false, true); for (JsonValidator v : getValidators()) { Set results = null; - Scope parentScope = collectorContext.enterDynamicScope(this); try { results = v.validate(executionContext, jsonNode, rootNode, instanceLocation); } finally { - Scope scope = collectorContext.exitDynamicScope(); if (results == null || results.isEmpty()) { - parentScope.mergeWith(scope); + // Do nothing if valid } else { + executionContext.getResults().setResult(instanceLocation, v.getSchemaLocation(), v.getEvaluationPath(), false); if (errors == null) { errors = new LinkedHashSet<>(); } errors.addAll(results); - if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator - || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { - collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems()); - } - if (v instanceof PropertiesValidator || v instanceof AdditionalPropertiesValidator - || v instanceof PatternPropertiesValidator) { - collectorContext.getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); - } } - } } @@ -804,6 +845,44 @@ public T validate(String input, InputFormat inputFormat, OutputFormat for }); } + /** + * Validates to a format. + * + * @param the result type + * @param executionContext the execution context + * @param node the node + * @param format the format + * @return the result + */ + public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format) { + return validate(executionContext, node, format, null); + } + + /** + * Validates to a format. + * + * @param the result type + * @param executionContext the execution context + * @param node the node + * @param format the format + * @param executionCustomizer the customizer + * @return the result + */ + public T validate(ExecutionContext executionContext, JsonNode node, OutputFormat format, + ExecutionContextCustomizer executionCustomizer) { + format.customize(executionContext, this.validationContext); + if (executionCustomizer != null) { + executionCustomizer.customize(executionContext, this.validationContext); + } + Set validationMessages = null; + try { + validationMessages = validate(executionContext, node); + } catch (FailFastAssertionException e) { + validationMessages = e.getValidationMessages(); + } + return format.format(this, validationMessages, executionContext, this.validationContext); + } + /** * Deserialize string to JsonNode. * @@ -956,7 +1035,6 @@ private ValidationResult walkAtNodeInternal(ExecutionContext executionContext, J public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { Set errors = new LinkedHashSet<>(); - CollectorContext collectorContext = executionContext.getCollectorContext(); // Walk through all the JSONWalker's. for (JsonValidator v : getValidators()) { JsonNodePath evaluationPathWithKeyword = v.getEvaluationPath(); @@ -968,23 +1046,12 @@ public Set walk(ExecutionContext executionContext, JsonNode n v.getEvaluationPath(), v.getSchemaLocation(), this.schemaNode, this.parentSchema, this.validationContext, this.validationContext.getJsonSchemaFactory())) { Set results = null; - Scope parentScope = collectorContext.enterDynamicScope(this); try { results = v.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } finally { - Scope scope = collectorContext.exitDynamicScope(); if (results == null || results.isEmpty()) { - parentScope.mergeWith(scope); } else { errors.addAll(results); - if (v instanceof PrefixItemsValidator || v instanceof ItemsValidator - || v instanceof ItemsValidator202012 || v instanceof ContainsValidator) { - collectorContext.getEvaluatedItems().addAll(scope.getEvaluatedItems()); - } - if (v instanceof PropertiesValidator || v instanceof AdditionalPropertiesValidator - || v instanceof PatternPropertiesValidator) { - collectorContext.getEvaluatedProperties().addAll(scope.getEvaluatedProperties()); - } } } } @@ -1079,13 +1146,13 @@ public boolean isRecursiveAnchor() { */ public ExecutionContext createExecutionContext() { SchemaValidatorsConfig config = validationContext.getConfig(); - CollectorContext collectorContext = new CollectorContext(config.isUnevaluatedItemsAnalysisDisabled(), - config.isUnevaluatedPropertiesAnalysisDisabled()); + CollectorContext collectorContext = new CollectorContext(); // Copy execution config defaults from validation config ExecutionConfig executionConfig = new ExecutionConfig(); executionConfig.setLocale(config.getLocale()); executionConfig.setFormatAssertionsEnabled(config.getFormatAssertionsEnabled()); + executionConfig.setFailFast(config.isFailFast()); ExecutionContext executionContext = new ExecutionContext(executionConfig, collectorContext); if(config.getExecutionContextCustomizer() != null) { diff --git a/src/main/java/com/networknt/schema/JsonSchemaException.java b/src/main/java/com/networknt/schema/JsonSchemaException.java index a95d1463e..2113d2e47 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaException.java +++ b/src/main/java/com/networknt/schema/JsonSchemaException.java @@ -24,7 +24,6 @@ public class JsonSchemaException extends RuntimeException { private ValidationMessage validationMessage; public JsonSchemaException(ValidationMessage validationMessage) { - super(validationMessage.getMessage()); this.validationMessage = validationMessage; } @@ -36,6 +35,15 @@ public JsonSchemaException(Throwable throwable) { super(throwable); } + @Override + public String getMessage() { + return this.validationMessage != null ? this.validationMessage.getMessage() : super.getMessage(); + } + + public ValidationMessage getValidationMessage() { + return this.validationMessage; + } + public Set getValidationMessages() { if (validationMessage == null) { return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java index f68d1d4e5..596332c7b 100644 --- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java +++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java @@ -255,7 +255,7 @@ public static Builder builder(final JsonSchemaFactory blueprint) { */ protected JsonSchema newJsonSchema(final SchemaLocation schemaUri, final JsonNode schemaNode, final SchemaValidatorsConfig config) { final ValidationContext validationContext = createValidationContext(schemaNode, config); - JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri, schemaNode, validationContext), + JsonSchema jsonSchema = doCreate(validationContext, getSchemaLocation(schemaUri), new JsonNodePath(validationContext.getConfig().getPathType()), schemaNode, null, false); try { /* @@ -306,20 +306,17 @@ private ValidationContext withMetaSchema(ValidationContext validationContext, Js } /** - * Gets the schema location from the $id or retrieval uri. + * Gets the base IRI from the schema retrieval IRI if present otherwise return + * one with a null base IRI. + *

+ * Note that the resolving of the $id or id in the schema node will take place + * in the JsonSchema constructor. * - * @param schemaRetrievalUri the schema retrieval uri - * @param schemaNode the schema json - * @param validationContext the validationContext + * @param schemaLocation the schema retrieval uri * @return the schema location */ - protected SchemaLocation getSchemaLocation(SchemaLocation schemaRetrievalUri, JsonNode schemaNode, - ValidationContext validationContext) { - String schemaLocation = validationContext.resolveSchemaId(schemaNode); - if (schemaLocation == null && schemaRetrievalUri != null) { - schemaLocation = schemaRetrievalUri.toString(); - } - return schemaLocation != null ? SchemaLocation.of(schemaLocation) : SchemaLocation.DOCUMENT; + protected SchemaLocation getSchemaLocation(SchemaLocation schemaLocation) { + return schemaLocation != null ? schemaLocation : SchemaLocation.DOCUMENT; } protected ValidationContext createValidationContext(final JsonNode schemaNode, SchemaValidatorsConfig config) { @@ -356,23 +353,29 @@ public JsonMetaSchema getMetaSchema(String id, SchemaValidatorsConfig config) { } protected JsonMetaSchema loadMetaSchema(String id, SchemaValidatorsConfig config) { - JsonSchema schema = getSchema(SchemaLocation.of(id), config); - JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema()); - VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification(); - if (specification != null) { - if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) { - // Process vocabularies - JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary"); - if (vocabulary != null) { - builder.vocabularies(new HashMap<>()); - for(Entry vocabs : vocabulary.properties()) { - builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); + try { + JsonSchema schema = getSchema(SchemaLocation.of(id), config); + JsonMetaSchema.Builder builder = JsonMetaSchema.builder(id, schema.getValidationContext().getMetaSchema()); + VersionFlag specification = schema.getValidationContext().getMetaSchema().getSpecification(); + if (specification != null) { + if (specification.getVersionFlagValue() >= VersionFlag.V201909.getVersionFlagValue()) { + // Process vocabularies + JsonNode vocabulary = schema.getSchemaNode().get("$vocabulary"); + if (vocabulary != null) { + builder.vocabularies(new HashMap<>()); + for(Entry vocabs : vocabulary.properties()) { + builder.vocabulary(vocabs.getKey(), vocabs.getValue().booleanValue()); + } } + } - } + return builder.build(); + } catch (Exception e) { + ValidationMessage validationMessage = ValidationMessage.builder().message("Unknown MetaSchema: {1}") + .arguments(id).build(); + throw new InvalidSchemaException(validationMessage, e); } - return builder.build(); } /** @@ -450,9 +453,21 @@ public JsonSchema getSchema(final InputStream schemaStream) { */ public JsonSchema getSchema(final SchemaLocation schemaUri, final SchemaValidatorsConfig config) { if (enableUriSchemaCache) { - JsonSchema cachedUriSchema = uriSchemaCache.computeIfAbsent(schemaUri, key -> { - return getMappedSchema(schemaUri, config); - }); + // ConcurrentHashMap computeIfAbsent does not allow calls that result in a + // recursive update to the map. + // The getMapperSchema potentially recurses to call back to getSchema again + JsonSchema cachedUriSchema = uriSchemaCache.get(schemaUri); + if (cachedUriSchema == null) { + synchronized (this) { // acquire lock on shared factory object to prevent deadlock + cachedUriSchema = uriSchemaCache.get(schemaUri); + if (cachedUriSchema == null) { + cachedUriSchema = getMappedSchema(schemaUri, config); + if (cachedUriSchema != null) { + uriSchemaCache.put(schemaUri, cachedUriSchema); + } + } + } + } return cachedUriSchema.withConfig(config); } return getMappedSchema(schemaUri, config); diff --git a/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java b/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java new file mode 100644 index 000000000..0629e43f4 --- /dev/null +++ b/src/main/java/com/networknt/schema/JsonSchemaIdValidator.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 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; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * Validator for validating the correctness of $id. + */ +public interface JsonSchemaIdValidator { + /** + * Validates if the $id value is valid. + * + * @param id the $id or id + * @param rootSchema true if this is a root schema + * @param schemaLocation the schema location + * @param resolvedSchemaLocation the schema location after resolving with the id + * @param validationContext the validation context for instance to get the + * meta schema + * @return true if valid + */ + boolean validate(String id, boolean rootSchema, SchemaLocation schemaLocation, + SchemaLocation resolvedSchemaLocation, ValidationContext validationContext); + + public static final JsonSchemaIdValidator DEFAULT = new DefaultJsonSchemaIdValidator(); + + /** + * Implementation of {@link JsonSchemaIdValidator}. + *

+ * Note that this does not strictly follow the specification. + *

+ * This allows an $id that isn't an absolute-IRI on the root schema but it must + * resolve to an absolute-IRI given a base-IRI. + *

+ * This also allows non-empty fragments. + */ + public static class DefaultJsonSchemaIdValidator implements JsonSchemaIdValidator { + @Override + public boolean validate(String id, boolean rootSchema, SchemaLocation schemaLocation, + SchemaLocation resolvedSchemaLocation, ValidationContext validationContext) { + if (hasNoContext(schemaLocation)) { + // The following are non-standard + if (isFragment(id) || startsWithSlash(id)) { + return true; + } + } + return resolvedSchemaLocation.getAbsoluteIri() != null + && isAbsoluteIri(resolvedSchemaLocation.getAbsoluteIri().toString()); + } + + protected boolean startsWithSlash(String id) { + return id.startsWith("/"); + } + + protected boolean isFragment(String id) { + return id.startsWith("#"); + } + + protected boolean hasNoContext(SchemaLocation schemaLocation) { + return schemaLocation.getAbsoluteIri() == null || schemaLocation.toString().startsWith("#"); + } + + protected boolean isAbsoluteIri(String iri) { + if (!iri.contains(":")) { + return false; // quick check + } + try { + new URI(iri); + } catch (URISyntaxException e) { + return false; + } + return true; + } + } +} diff --git a/src/main/java/com/networknt/schema/Keyword.java b/src/main/java/com/networknt/schema/Keyword.java index bf9b4c45a..592c80384 100644 --- a/src/main/java/com/networknt/schema/Keyword.java +++ b/src/main/java/com/networknt/schema/Keyword.java @@ -18,9 +18,29 @@ import com.fasterxml.jackson.databind.JsonNode; +/** + * Represents a keyword. + */ public interface Keyword { + /** + * Gets the keyword value. + * + * @return the keyword value + */ String getValue(); + /** + * Creates a new validator for the keyword. + * + * @param schemaLocation the schema location + * @param evaluationPath the evaluation path + * @param schemaNode the schema node + * @param parentSchema the parent schema + * @param validationContext the validation context + * @return the validation + * @throws JsonSchemaException the exception + * @throws Exception the exception + */ JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception; } diff --git a/src/main/java/com/networknt/schema/MaxItemsValidator.java b/src/main/java/com/networknt/schema/MaxItemsValidator.java index 63f6ccca5..a232349cf 100644 --- a/src/main/java/com/networknt/schema/MaxItemsValidator.java +++ b/src/main/java/com/networknt/schema/MaxItemsValidator.java @@ -23,11 +23,13 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for maxItems. + */ public class MaxItemsValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MaxItemsValidator.class); - private int max = 0; public MaxItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { @@ -42,11 +44,15 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() > max) { - return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build()); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 > max) { - return Collections.singleton(message().instanceLocation(instanceLocation).locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build()); } } diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java index b93be3137..07e7b79d0 100644 --- a/src/main/java/com/networknt/schema/MaxLengthValidator.java +++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for maxLength. + */ public class MaxLengthValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MaxLengthValidator.class); @@ -45,8 +48,9 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } if (node.textValue().codePointCount(0, node.textValue().length()) > maxLength) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(maxLength).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(maxLength).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java index 56eadd1f1..b50b6c57d 100644 --- a/src/main/java/com/networknt/schema/MaxPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MaxPropertiesValidator.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator}for maxProperties. + */ public class MaxPropertiesValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MaxPropertiesValidator.class); @@ -41,8 +44,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() > max) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(max).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(max).build()); } } diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java index 3bc086073..0137f3b36 100644 --- a/src/main/java/com/networknt/schema/MaximumValidator.java +++ b/src/main/java/com/networknt/schema/MaximumValidator.java @@ -27,6 +27,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for maxmimum. + */ public class MaximumValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(MaximumValidator.class); private static final String PROPERTY_EXCLUSIVE_MAXIMUM = "exclusiveMaximum"; @@ -113,9 +116,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMaximum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMaximum.thresholdValue()) - .build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(typedMaximum.thresholdValue()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java index 69555763f..2257deb0a 100644 --- a/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java +++ b/src/main/java/com/networknt/schema/MessageSourceValidationMessage.java @@ -17,20 +17,20 @@ import java.util.Locale; import java.util.Map; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import com.networknt.schema.i18n.MessageSource; public class MessageSourceValidationMessage { public static Builder builder(MessageSource messageSource, Map errorMessage, - Consumer observer) { + BiConsumer observer) { return new Builder(messageSource, errorMessage, observer); } public static class Builder extends BuilderSupport { public Builder(MessageSource messageSource, Map errorMessage, - Consumer observer) { + BiConsumer observer) { super(messageSource, errorMessage, observer); } @@ -41,13 +41,14 @@ public Builder self() { } public abstract static class BuilderSupport extends ValidationMessage.BuilderSupport { - private final Consumer observer; + private final BiConsumer observer; private final MessageSource messageSource; private final Map errorMessage; + private boolean failFast; private Locale locale; public BuilderSupport(MessageSource messageSource, Map errorMessage, - Consumer observer) { + BiConsumer observer) { this.messageSource = messageSource; this.observer = observer; this.errorMessage = errorMessage; @@ -75,7 +76,7 @@ public ValidationMessage build() { } ValidationMessage validationMessage = super.build(); if (this.observer != null) { - this.observer.accept(validationMessage); + this.observer.accept(validationMessage, this.failFast); } return validationMessage; } @@ -84,5 +85,10 @@ public S locale(Locale locale) { this.locale = locale; return self(); } + + public S failFast(boolean failFast) { + this.failFast = failFast; + return self(); + } } } diff --git a/src/main/java/com/networknt/schema/MinItemsValidator.java b/src/main/java/com/networknt/schema/MinItemsValidator.java index 969b399cb..85e833db8 100644 --- a/src/main/java/com/networknt/schema/MinItemsValidator.java +++ b/src/main/java/com/networknt/schema/MinItemsValidator.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for minItems. + */ public class MinItemsValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MinItemsValidator.class); @@ -40,13 +43,16 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isArray()) { if (node.size() < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, node.size()).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min, node.size()) + .build()); } } else if (this.validationContext.getConfig().isTypeLoose()) { if (1 < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min, 1).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min, 1).build()); } } diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java index 7038a2bd1..c5dc0435a 100644 --- a/src/main/java/com/networknt/schema/MinLengthValidator.java +++ b/src/main/java/com/networknt/schema/MinLengthValidator.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for minLength. + */ public class MinLengthValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MinLengthValidator.class); @@ -46,8 +49,9 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (node.textValue().codePointCount(0, node.textValue().length()) < minLength) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(minLength).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(minLength).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java index 419ca6f1c..46db1447e 100644 --- a/src/main/java/com/networknt/schema/MinMaxContainsValidator.java +++ b/src/main/java/com/networknt/schema/MinMaxContainsValidator.java @@ -8,7 +8,7 @@ import java.util.stream.Collectors; /** - * Tests the validity of {@literal maxContains} and {@literal minContains} in a schema. + * {@link JsonValidator} for {@literal maxContains} and {@literal minContains} in a schema. *

* This validator only checks that the schema is valid. The functionality for * testing whether an instance array conforms to the {@literal maxContains} @@ -62,8 +62,10 @@ public MinMaxContainsValidator(SchemaLocation schemaLocation, JsonNodePath evalu public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { return this.analysis != null ? this.analysis.stream() - .map(analysis -> message().instanceLocation(analysis.getSchemaLocation().getFragment()) + .map(analysis -> message().instanceNode(node) + .instanceLocation(analysis.getSchemaLocation().getFragment()) .messageKey(analysis.getMessageKey()).locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(parentSchema.getSchemaNode().toString()).build()) .collect(Collectors.toCollection(LinkedHashSet::new)) : Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MinPropertiesValidator.java b/src/main/java/com/networknt/schema/MinPropertiesValidator.java index 80e41f798..17b4fbe3c 100644 --- a/src/main/java/com/networknt/schema/MinPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/MinPropertiesValidator.java @@ -23,6 +23,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for minProperties. + */ public class MinPropertiesValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MinPropertiesValidator.class); @@ -41,8 +44,9 @@ public Set validate(ExecutionContext executionContext, JsonNo if (node.isObject()) { if (node.size() < min) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(min).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(min).build()); } } diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java index 27ff40253..3e43624d5 100644 --- a/src/main/java/com/networknt/schema/MinimumValidator.java +++ b/src/main/java/com/networknt/schema/MinimumValidator.java @@ -27,6 +27,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for minimum. + */ public class MinimumValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(MinimumValidator.class); private static final String PROPERTY_EXCLUSIVE_MINIMUM = "exclusiveMinimum"; @@ -120,9 +123,10 @@ public Set validate(ExecutionContext executionContext, JsonNo } if (typedMinimum.crossesThreshold(node)) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(typedMinimum.thresholdValue()) - .build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(typedMinimum.thresholdValue()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/MultipleOfValidator.java b/src/main/java/com/networknt/schema/MultipleOfValidator.java index 7d1d73f45..e91f2d4e4 100644 --- a/src/main/java/com/networknt/schema/MultipleOfValidator.java +++ b/src/main/java/com/networknt/schema/MultipleOfValidator.java @@ -24,6 +24,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for multipleOf. + */ public class MultipleOfValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(MultipleOfValidator.class); @@ -46,8 +49,9 @@ public Set validate(ExecutionContext executionContext, JsonNo BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue)); BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor)); if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(divisor).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(divisor).build()); } } } diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java index ef1b8d9f5..c5af300b1 100644 --- a/src/main/java/com/networknt/schema/NonValidationKeyword.java +++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java @@ -27,14 +27,19 @@ * Used for Keywords that have no validation aspect, but are part of the metaschema. */ public class NonValidationKeyword extends AbstractKeyword { + private final boolean collectAnnotations; private static final class Validator extends AbstractJsonValidator { + private final boolean collectAnnotations; + public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, - JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword) { - super(schemaLocation, evaluationPath, keyword); + JsonSchema parentSchema, ValidationContext validationContext, Keyword keyword, boolean collectAnnotations) { + super(schemaLocation, evaluationPath, keyword, schemaNode); + this.collectAnnotations = collectAnnotations; String id = validationContext.resolveSchemaId(schemaNode); String anchor = validationContext.getMetaSchema().readAnchor(schemaNode); - if (id != null || anchor != null) { + String dynamicAnchor = validationContext.getMetaSchema().readDynamicAnchor(schemaNode); + if (id != null || anchor != null || dynamicAnchor != null) { // Used to register schema resources with $id validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } @@ -49,17 +54,40 @@ public Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, Jso @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { + if (collectAnnotations && collectAnnotations(executionContext)) { + Object value = getAnnotationValue(getSchemaNode()); + if (value != null) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).value(value)); + } + } return Collections.emptySet(); } + + protected Object getAnnotationValue(JsonNode schemaNode) { + if (schemaNode.isTextual()) { + return schemaNode.textValue(); + } else if (schemaNode.isNumber()) { + return schemaNode.numberValue(); + } else if (schemaNode.isObject()) { + return schemaNode; + } + return null; + } } public NonValidationKeyword(String keyword) { + this(keyword, true); + } + + public NonValidationKeyword(String keyword, boolean collectAnnotations) { super(keyword); + this.collectAnnotations = collectAnnotations; } @Override public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { - return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this); + return new Validator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, this, collectAnnotations); } } diff --git a/src/main/java/com/networknt/schema/NotAllowedValidator.java b/src/main/java/com/networknt/schema/NotAllowedValidator.java index c6c4e80fe..baf8dd762 100644 --- a/src/main/java/com/networknt/schema/NotAllowedValidator.java +++ b/src/main/java/com/networknt/schema/NotAllowedValidator.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} for notAllowed. + */ public class NotAllowedValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(NotAllowedValidator.class); @@ -49,8 +52,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (errors == null) { errors = new LinkedHashSet<>(); } - errors.add(message().property(fieldName).instanceLocation(instanceLocation.append(fieldName)) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); + errors.add(message().property(fieldName).instanceNode(node) + .instanceLocation(instanceLocation.append(fieldName)) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(fieldName).build()); } } diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java index 6e5164301..9a2c2e36e 100644 --- a/src/main/java/com/networknt/schema/NotValidator.java +++ b/src/main/java/com/networknt/schema/NotValidator.java @@ -17,13 +17,15 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} for not. + */ public class NotValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(NotValidator.class); @@ -36,25 +38,25 @@ public NotValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - Set errors = new HashSet<>(); + Set errors = null; + debug(logger, node, rootNode, instanceLocation); - Scope parentScope = collectorContext.enterDynamicScope(); + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); try { - debug(logger, node, rootNode, instanceLocation); + executionContext.getExecutionConfig().setFailFast(false); errors = this.schema.validate(executionContext, node, rootNode, instanceLocation); - if (errors.isEmpty()) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) - .build()); - } - return Collections.emptySet(); } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } + if (errors.isEmpty()) { + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString()) + .build()); + } + return Collections.emptySet(); } @Override @@ -65,8 +67,9 @@ public Set walk(ExecutionContext executionContext, JsonNode n Set errors = this.schema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); if (errors.isEmpty()) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.schema.toString()) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.schema.toString()) .build()); } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index e7e8eb7b4..1fe74a56e 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -17,18 +17,22 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} for oneOf. + */ public class OneOfValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(OneOfValidator.class); private final List schemas = new ArrayList<>(); + private Boolean canShortCircuit = null; + public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext); int size = schemaNode.size(); @@ -41,85 +45,90 @@ public OneOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { Set errors = new LinkedHashSet<>(); - CollectorContext collectorContext = executionContext.getCollectorContext(); - Scope grandParentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); + debug(logger, node, rootNode, instanceLocation); - ValidatorState state = executionContext.getValidatorState(); + ValidatorState state = executionContext.getValidatorState(); - // this is a complex validator, we set the flag to true - state.setComplexValidator(true); + // this is a complex validator, we set the flag to true + state.setComplexValidator(true); - int numberOfValidSchema = 0; - Set childErrors = new LinkedHashSet<>(); + int numberOfValidSchema = 0; + Set childErrors = new LinkedHashSet<>(); + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); for (JsonSchema schema : this.schemas) { Set schemaErrors = Collections.emptySet(); - Scope parentScope = collectorContext.enterDynamicScope(); - try { - // Reset state in case the previous validator did not match - state.setMatchedNode(true); - - if (!state.isWalkEnabled()) { - schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation); - } else { - schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation, state.isValidationEnabled()); - } - - // check if any validation errors have occurred - if (schemaErrors.isEmpty()) { - // check whether there are no errors HOWEVER we have validated the exact validator - if (!state.hasMatchedNode()) - continue; - - numberOfValidSchema++; - } + // Reset state in case the previous validator did not match + state.setMatchedNode(true); - if (numberOfValidSchema > 1) { - // short-circuit - break; - } + if (!state.isWalkEnabled()) { + schemaErrors = schema.validate(executionContext, node, rootNode, instanceLocation); + } else { + schemaErrors = schema.walk(executionContext, node, rootNode, instanceLocation, + state.isValidationEnabled()); + } - childErrors.addAll(schemaErrors); - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (schemaErrors.isEmpty()) { - parentScope.mergeWith(scope); + // check if any validation errors have occurred + if (schemaErrors.isEmpty()) { + // check whether there are no errors HOWEVER we have validated the exact + // validator + if (!state.hasMatchedNode()) { + continue; } + numberOfValidSchema++; } - } - // ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1. - if (numberOfValidSchema != 1) { - ValidationMessage message = message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()) - .arguments(Integer.toString(numberOfValidSchema)).build(); - if (this.failFast) { - throw new JsonSchemaException(message); + if (numberOfValidSchema > 1 && canShortCircuit()) { + // short-circuit + break; } - errors.add(message); - errors.addAll(childErrors); - collectorContext.getEvaluatedItems().clear(); - collectorContext.getEvaluatedProperties().clear(); + + childErrors.addAll(schemaErrors); } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); + } - // Make sure to signal parent handlers we matched - if (errors.isEmpty()) - state.setMatchedNode(true); + // ensure there is always an "OneOf" error reported if number of valid schemas + // is not equal to 1. + if (numberOfValidSchema != 1) { + ValidationMessage message = message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(Integer.toString(numberOfValidSchema)).build(); + errors.add(message); + errors.addAll(childErrors); + } - // reset the ValidatorState object - resetValidatorState(executionContext); + // Make sure to signal parent handlers we matched + if (errors.isEmpty()) { + state.setMatchedNode(true); + } - return Collections.unmodifiableSet(errors); - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - grandParentScope.mergeWith(scope); + // reset the ValidatorState object + resetValidatorState(executionContext); + + return Collections.unmodifiableSet(errors); + } + + protected boolean canShortCircuit() { + if (this.canShortCircuit == null) { + boolean canShortCircuit = true; + for (JsonValidator validator : getEvaluationParentSchema().getValidators()) { + if ("unevaluatedProperties".equals(validator.getKeyword()) + || "unevaluatedItems".equals(validator.getKeyword())) { + canShortCircuit = false; + } } + this.canShortCircuit = canShortCircuit; } + return this.canShortCircuit; } private static void resetValidatorState(ExecutionContext executionContext) { @@ -146,5 +155,6 @@ public void preloadJsonSchema() { for (JsonSchema schema: this.schemas) { schema.initializeValidators(); } + canShortCircuit(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/OutputFormat.java b/src/main/java/com/networknt/schema/OutputFormat.java index 6f8e84d05..b3c56e7ad 100644 --- a/src/main/java/com/networknt/schema/OutputFormat.java +++ b/src/main/java/com/networknt/schema/OutputFormat.java @@ -17,6 +17,11 @@ import java.util.Set; +import com.networknt.schema.output.HierarchicalOutputUnitFormatter; +import com.networknt.schema.output.ListOutputUnitFormatter; +import com.networknt.schema.output.OutputFlag; +import com.networknt.schema.output.OutputUnit; + /** * Formats the validation results. * @@ -37,13 +42,15 @@ default void customize(ExecutionContext executionContext, ValidationContext vali /** * Formats the validation results. * + * @param jsonSchema the schema * @param validationMessages the validation messages - * @param executionContext the execution context - * @param validationContext the validation context + * @param executionContext the execution context + * @param validationContext the validation context + * * @return the result */ - T format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext); + T format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext); /** * The Default output format. @@ -59,6 +66,17 @@ T format(Set validationMessages, ExecutionContext executionCo * The Flag output format. */ public static final Flag FLAG = new Flag(); + + /** + * The List output format. + */ + public static final List LIST = new List(); + + + /** + * The Hierarchical output format. + */ + public static final Hierarchical HIERARCHICAL = new Hierarchical(); /** * The Default output format. @@ -66,13 +84,12 @@ T format(Set validationMessages, ExecutionContext executionCo public static class Default implements OutputFormat> { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); } @Override - public Set format(Set validationMessages, - ExecutionContext executionContext, ValidationContext validationContext) { + public Set format(JsonSchema jsonSchema, + Set validationMessages, ExecutionContext executionContext, ValidationContext validationContext) { return validationMessages; } } @@ -80,17 +97,17 @@ public Set format(Set validationMessages, /** * The Flag output format. */ - public static class Flag implements OutputFormat { + public static class Flag implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); + executionContext.getExecutionConfig().setFailFast(true); } @Override - public FlagOutput format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext) { - return new FlagOutput(validationMessages.isEmpty()); + public OutputFlag format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { + return new OutputFlag(validationMessages.isEmpty()); } } @@ -100,29 +117,44 @@ public FlagOutput format(Set validationMessages, ExecutionCon public static class Boolean implements OutputFormat { @Override public void customize(ExecutionContext executionContext, ValidationContext validationContext) { - executionContext.getExecutionConfig().setAnnotationAllowedPredicate( - Annotations.getDefaultAnnotationAllowListPredicate(validationContext.getMetaSchema())); + executionContext.getExecutionConfig().setAnnotationCollectionEnabled(false); + executionContext.getExecutionConfig().setFailFast(true); } @Override - public java.lang.Boolean format(Set validationMessages, ExecutionContext executionContext, - ValidationContext validationContext) { + public java.lang.Boolean format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { return validationMessages.isEmpty(); } } - + /** - * The Flag output results. + * The List output format. */ - public static class FlagOutput { - private final boolean valid; + public static class List implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + } - public FlagOutput(boolean valid) { - this.valid = valid; + @Override + public OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { + return ListOutputUnitFormatter.format(validationMessages, executionContext, validationContext); } + } - public boolean isValid() { - return this.valid; + /** + * The Hierarchical output format. + */ + public static class Hierarchical implements OutputFormat { + @Override + public void customize(ExecutionContext executionContext, ValidationContext validationContext) { + } + + @Override + public OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { + return HierarchicalOutputUnitFormatter.format(jsonSchema, validationMessages, executionContext, validationContext); } } } diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java index ceeb6c2b8..1e5bc18c9 100644 --- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java @@ -17,17 +17,23 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.regex.RegularExpression; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} for patternProperties. + */ public class PatternPropertiesValidator extends BaseJsonValidator { public static final String PROPERTY = "patternProperties"; private static final Logger logger = LoggerFactory.getLogger(PatternPropertiesValidator.class); private final Map schemas = new IdentityHashMap<>(); + private Boolean hasUnevaluatedPropertiesValidator = null; + public PatternPropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PATTERN_PROPERTIES, validationContext); @@ -50,7 +56,9 @@ public Set validate(ExecutionContext executionContext, JsonNo return Collections.emptySet(); } Set errors = null; + Set matchedInstancePropertyNames = null; Iterator names = node.fieldNames(); + boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); while (names.hasNext()) { String name = names.next(); JsonNode n = node.get(name); @@ -59,8 +67,11 @@ public Set validate(ExecutionContext executionContext, JsonNo JsonNodePath path = instanceLocation.append(name); Set results = entry.getValue().validate(executionContext, n, rootNode, path); if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - executionContext.getCollectorContext().getEvaluatedProperties().add(path); + if (collectAnnotations) { + if (matchedInstancePropertyNames == null) { + matchedInstancePropertyNames = new LinkedHashSet<>(); + } + matchedInstancePropertyNames.add(name); } } else { if (errors == null) { @@ -71,11 +82,29 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + if (collectAnnotations) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(matchedInstancePropertyNames).build()); + } return errors == null ? Collections.emptySet() : Collections.unmodifiableSet(errors); } + + private boolean collectAnnotations() { + return hasUnevaluatedPropertiesValidator(); + } + + private boolean hasUnevaluatedPropertiesValidator() { + if (this.hasUnevaluatedPropertiesValidator == null) { + this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties"); + } + return hasUnevaluatedPropertiesValidator; + } @Override public void preloadJsonSchema() { preloadJsonSchemas(schemas.values()); + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java index 71247cf95..9b7f087a8 100644 --- a/src/main/java/com/networknt/schema/PatternValidator.java +++ b/src/main/java/com/networknt/schema/PatternValidator.java @@ -58,8 +58,9 @@ public Set validate(ExecutionContext executionContext, JsonNo try { if (!matches(node.asText())) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(this.pattern).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.pattern).build()); } } catch (JsonSchemaException e) { throw e; diff --git a/src/main/java/com/networknt/schema/PrefixItemsValidator.java b/src/main/java/com/networknt/schema/PrefixItemsValidator.java index dc2b00a55..230f67798 100644 --- a/src/main/java/com/networknt/schema/PrefixItemsValidator.java +++ b/src/main/java/com/networknt/schema/PrefixItemsValidator.java @@ -18,23 +18,28 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.walk.DefaultItemWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +/** + * {@link JsonValidator} for prefixItems. + */ public class PrefixItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(PrefixItemsValidator.class); private final List tupleSchema; private WalkListenerRunner arrayItemWalkListenerRunner; + + private Boolean hasUnevaluatedItemsValidator = null; public PrefixItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PREFIX_ITEMS, validationContext); @@ -58,19 +63,34 @@ public Set validate(ExecutionContext executionContext, JsonNo // ignores non-arrays if (node.isArray()) { Set errors = new LinkedHashSet<>(); - Collection evaluatedItems = executionContext.getCollectorContext().getEvaluatedItems(); int count = Math.min(node.size(), this.tupleSchema.size()); for (int i = 0; i < count; ++i) { JsonNodePath path = instanceLocation.append(i); Set results = this.tupleSchema.get(i).validate(executionContext, node.get(i), rootNode, path); - if (results.isEmpty()) { - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - evaluatedItems.add(path); - } - } else { + if (!results.isEmpty()) { errors.addAll(results); } } + + // Add annotation + if (collectAnnotations() || collectAnnotations(executionContext)) { + // Tuples + int items = node.isArray() ? node.size() : 1; + int schemas = this.tupleSchema.size(); + if (items > schemas) { + // More items than schemas so the keyword only applied to the number of schemas + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(schemas).build()); + } else { + // Applies to all + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(true).build()); + } + } return errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } else { return Collections.emptySet(); @@ -140,9 +160,21 @@ public List getTupleSchema() { return this.tupleSchema; } + private boolean collectAnnotations() { + return hasUnevaluatedItemsValidator(); + } + + private boolean hasUnevaluatedItemsValidator() { + if (this.hasUnevaluatedItemsValidator == null) { + this.hasUnevaluatedItemsValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedItems"); + } + return hasUnevaluatedItemsValidator; + } + @Override public void preloadJsonSchema() { preloadJsonSchemas(this.tupleSchema); + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java index cd6534f74..f3ce77719 100644 --- a/src/main/java/com/networknt/schema/PropertiesValidator.java +++ b/src/main/java/com/networknt/schema/PropertiesValidator.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; import com.networknt.schema.walk.DefaultPropertyWalkListenerRunner; import com.networknt.schema.walk.WalkListenerRunner; import org.slf4j.Logger; @@ -26,10 +27,15 @@ import java.util.*; +/** + * {@link JsonValidator} for properties. + */ public class PropertiesValidator extends BaseJsonValidator { public static final String PROPERTY = "properties"; private static final Logger logger = LoggerFactory.getLogger(PropertiesValidator.class); private final Map schemas = new LinkedHashMap<>(); + + private Boolean hasUnevaluatedPropertiesValidator; public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext); @@ -43,7 +49,6 @@ public PropertiesValidator(SchemaLocation schemaLocation, JsonNodePath evaluatio @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); WalkListenerRunner propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(this.validationContext.getConfig().getPropertyWalkListeners()); @@ -52,15 +57,19 @@ public Set validate(ExecutionContext executionContext, JsonNo // get the Validator state object storing validation data ValidatorState state = executionContext.getValidatorState(); - Set requiredErrors = null; - + Set requiredErrors = null; + Set matchedInstancePropertyNames = null; + boolean collectAnnotations = collectAnnotations() || collectAnnotations(executionContext); for (Map.Entry entry : this.schemas.entrySet()) { JsonSchema propertySchema = entry.getValue(); JsonNode propertyNode = node.get(entry.getKey()); if (propertyNode != null) { JsonNodePath path = instanceLocation.append(entry.getKey()); - if (executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword())) { - collectorContext.getEvaluatedProperties().add(path); // TODO: This should happen after validation + if (collectAnnotations) { + if (matchedInstancePropertyNames == null) { + matchedInstancePropertyNames = new LinkedHashSet<>(); + } + matchedInstancePropertyNames.add(entry.getKey()); } // check whether this is a complex validator. save the state boolean isComplex = state.isComplexValidator(); @@ -70,7 +79,7 @@ public Set validate(ExecutionContext executionContext, JsonNo } // reset the complex validator for child element validation, and reset it after the return from the recursive call state.setComplexValidator(false); - + if (!state.isWalkEnabled()) { //validate the child element(s) Set result = propertySchema.validate(executionContext, propertyNode, rootNode, path); @@ -119,6 +128,15 @@ public Set validate(ExecutionContext executionContext, JsonNo } } } + if (collectAnnotations) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword(getKeyword()).value(matchedInstancePropertyNames == null ? Collections.emptySet() + : matchedInstancePropertyNames) + .build()); + } + return errors == null || errors.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(errors); } @@ -139,6 +157,17 @@ public Set walk(ExecutionContext executionContext, JsonNode n return validationMessages; } + private boolean collectAnnotations() { + return hasUnevaluatedPropertiesValidator(); + } + + private boolean hasUnevaluatedPropertiesValidator() { + if (this.hasUnevaluatedPropertiesValidator == null) { + this.hasUnevaluatedPropertiesValidator = hasAdjacentKeywordInEvaluationPath("unevaluatedProperties"); + } + return hasUnevaluatedPropertiesValidator; + } + private void applyPropertyDefaults(ObjectNode node) { for (Map.Entry entry : this.schemas.entrySet()) { JsonNode propertyNode = node.get(entry.getKey()); @@ -188,5 +217,6 @@ public Map getSchemas() { @Override public void preloadJsonSchema() { preloadJsonSchemas(this.schemas.values()); + collectAnnotations(); // cache the flag } } diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java index 96d9ed3ea..12465d34b 100644 --- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java +++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java @@ -48,8 +48,10 @@ public Set validate(ExecutionContext executionContext, JsonNo if (msg.startsWith(path)) msg = msg.substring(path.length()).replaceFirst("^:\\s*", ""); - errors.add(message().property(pname).instanceLocation(schemaError.getInstanceLocation()) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(msg).build()); + errors.add( + message().property(pname).instanceNode(node).instanceLocation(schemaError.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(msg).build()); } } return Collections.unmodifiableSet(errors); diff --git a/src/main/java/com/networknt/schema/ReadOnlyValidator.java b/src/main/java/com/networknt/schema/ReadOnlyValidator.java index a3d1e4f22..bf2759bfe 100644 --- a/src/main/java/com/networknt/schema/ReadOnlyValidator.java +++ b/src/main/java/com/networknt/schema/ReadOnlyValidator.java @@ -24,6 +24,9 @@ import com.fasterxml.jackson.databind.JsonNode; +/** + * {@link JsonValidator} for readOnly. + */ public class ReadOnlyValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(ReadOnlyValidator.class); @@ -40,8 +43,9 @@ public ReadOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); if (this.readOnly) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/RecursiveRefValidator.java b/src/main/java/com/networknt/schema/RecursiveRefValidator.java index 16714be22..0ca0f26fb 100644 --- a/src/main/java/com/networknt/schema/RecursiveRefValidator.java +++ b/src/main/java/com/networknt/schema/RecursiveRefValidator.java @@ -17,84 +17,132 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} that resolves $recursiveRef. + */ public class RecursiveRefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RecursiveRefValidator.class); - private Map schemas = new HashMap<>(); + protected JsonSchemaRef schema; public RecursiveRefValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.RECURSIVE_REF, validationContext); String refValue = schemaNode.asText(); if (!"#".equals(refValue)) { - ValidationMessage validationMessage = ValidationMessage.builder() + ValidationMessage validationMessage = message() .type(ValidatorTypeCode.RECURSIVE_REF.getValue()).code("internal.invalidRecursiveRef") .message("{0}: The value of a $recursiveRef must be '#' but is '{1}'").instanceLocation(schemaLocation.getFragment()) - .evaluationPath(schemaLocation.getFragment()).arguments(refValue).build(); + .instanceNode(this.schemaNode) + .evaluationPath(evaluationPath).arguments(refValue).build(); throw new JsonSchemaException(validationMessage); } + this.schema = getRefSchema(parentSchema, validationContext, refValue, evaluationPath); } + static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, + JsonNodePath evaluationPath) { + return new JsonSchemaRef(new CachedSupplier<>(() -> { + return getSchema(parentSchema, validationContext, refValue, evaluationPath); + })); + } + + static JsonSchema getSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue, + JsonNodePath evaluationPath) { + JsonSchema refSchema = parentSchema.findSchemaResourceRoot(); // Get the document + JsonSchema current = refSchema; + JsonSchema check = null; + String base = null; + String baseCheck = null; + if (refSchema != null) + base = current.getSchemaLocation().getAbsoluteIri() != null ? current.getSchemaLocation().getAbsoluteIri().toString() : ""; + if (current.isRecursiveAnchor()) { + // Check dynamic scope + while (current.getEvaluationParentSchema() != null) { + current = current.getEvaluationParentSchema(); + baseCheck = current.getSchemaLocation().getAbsoluteIri() != null ? current.getSchemaLocation().getAbsoluteIri().toString() : ""; + if (!base.equals(baseCheck)) { + base = baseCheck; + // Check if it has a dynamic anchor + check = current.findSchemaResourceRoot(); + if (check.isRecursiveAnchor()) { + refSchema = check; + } + } + } + } + if (refSchema != null) { + refSchema = refSchema.fromRef(parentSchema, evaluationPath); + } + return refSchema; + } + @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = new HashSet<>(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); - - JsonSchema schema = collectorContext.getOutermostSchema(); - if (null != schema) { - JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> { - return schema.fromRef(getParentSchema(), getEvaluationPath()); - }); - errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); - } - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + debug(logger, node, rootNode, instanceLocation); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.RECURSIVE_REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); } - - return errors; + return refSchema.validate(executionContext, node, rootNode, instanceLocation); } @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - CollectorContext collectorContext = executionContext.getCollectorContext(); + debug(logger, node, rootNode, instanceLocation); + // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, + // these schemas will be cached along with config. We have to replace the config for cached $ref references + // with the latest config. Reset the config. + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.RECURSIVE_REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); + } + return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); + } - Set errors = new HashSet<>(); + public JsonSchemaRef getSchemaRef() { + return this.schema; + } - Scope parentScope = collectorContext.enterDynamicScope(); + @Override + public void preloadJsonSchema() { + JsonSchema jsonSchema = null; try { - debug(logger, node, rootNode, instanceLocation); - - JsonSchema schema = collectorContext.getOutermostSchema(); - if (null != schema) { - JsonSchema refSchema = schemas.computeIfAbsent(schema.getSchemaLocation(), key -> { - return schema.fromRef(getParentSchema(), getEvaluationPath()); - }); - errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); - } - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + jsonSchema = this.schema.getSchema(); + } catch (JsonSchemaException e) { + throw e; + } catch (RuntimeException e) { + throw new JsonSchemaException(e); + } + // Check for circular dependency + // Only one cycle is pre-loaded + // The rest of the cycles will load at execution time depending on the input + // data + SchemaLocation schemaLocation = jsonSchema.getSchemaLocation(); + JsonSchema check = jsonSchema; + boolean circularDependency = false; + while (check.getEvaluationParentSchema() != null) { + check = check.getEvaluationParentSchema(); + if (check.getSchemaLocation().equals(schemaLocation)) { + circularDependency = true; + break; } } - - return errors; + if (!circularDependency) { + jsonSchema.initializeValidators(); + } } - } diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java index e8c5016e1..1959231e3 100644 --- a/src/main/java/com/networknt/schema/RefValidator.java +++ b/src/main/java/com/networknt/schema/RefValidator.java @@ -17,12 +17,14 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; -import com.networknt.schema.CollectorContext.Scope; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +/** + * {@link JsonValidator} that resolves $ref. + */ public class RefValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(RefValidator.class); @@ -77,7 +79,7 @@ static JsonSchemaRef getRefSchema(JsonSchema parentSchema, ValidationContext val findSchemaResource = validationContext.getDynamicAnchors().get(find); } if (findSchemaResource != null) { - schemaResource = findSchemaResource; + schemaResource = findSchemaResource; } else { schemaResource = getJsonSchema(schemaResource, validationContext, newRefValue, refValueOriginal, evaluationPath); @@ -147,75 +149,59 @@ private static JsonSchema getJsonSchema(JsonSchema parent, // This should be processing json pointer fragments only JsonNodePath fragment = SchemaLocation.Fragment.of(refValue); String schemaReference = resolve(parent, refValueOriginal); - return validationContext.getSchemaReferences().computeIfAbsent(schemaReference, key -> { - return parent.getSubSchema(fragment); - }); + // ConcurrentHashMap computeIfAbsent does not allow calls that result in a + // recursive update to the map. + // The getSubSchema potentially recurses to call back to getJsonSchema again + JsonSchema result = validationContext.getSchemaReferences().get(schemaReference); + if (result == null) { + synchronized (validationContext.getJsonSchemaFactory()) { // acquire lock on shared factory object to prevent deadlock + result = validationContext.getSchemaReferences().get(schemaReference); + if (result == null) { + result = parent.getSubSchema(fragment); + if (result != null) { + validationContext.getSchemaReferences().put(schemaReference, result); + } + } + } + } + return result; } @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = Collections.emptySet(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) - .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) - .arguments(schemaNode.asText()).build(); - throw new JsonSchemaException(validationMessage); - } - errors = refSchema.validate(executionContext, node, rootNode, instanceLocation); - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } + debug(logger, node, rootNode, instanceLocation); + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); } - return errors; + return refSchema.validate(executionContext, node, rootNode, instanceLocation); } @Override public Set walk(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean shouldValidateSchema) { - CollectorContext collectorContext = executionContext.getCollectorContext(); - - Set errors = Collections.emptySet(); - - Scope parentScope = collectorContext.enterDynamicScope(); - try { - debug(logger, node, rootNode, instanceLocation); - // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, - // these schemas will be cached along with config. We have to replace the config for cached $ref references - // with the latest config. Reset the config. - JsonSchema refSchema = this.schema.getSchema(); - if (refSchema == null) { - ValidationMessage validationMessage = ValidationMessage.builder().type(ValidatorTypeCode.REF.getValue()) - .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") - .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) - .arguments(schemaNode.asText()).build(); - throw new JsonSchemaException(validationMessage); - } - errors = refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); - return errors; - } finally { - Scope scope = collectorContext.exitDynamicScope(); - if (shouldValidateSchema) { - if (errors.isEmpty()) { - parentScope.mergeWith(scope); - } - } + debug(logger, node, rootNode, instanceLocation); + // This is important because if we use same JsonSchemaFactory for creating multiple JSONSchema instances, + // these schemas will be cached along with config. We have to replace the config for cached $ref references + // with the latest config. Reset the config. + JsonSchema refSchema = this.schema.getSchema(); + if (refSchema == null) { + ValidationMessage validationMessage = message().type(ValidatorTypeCode.REF.getValue()) + .code("internal.unresolvedRef").message("{0}: Reference {1} cannot be resolved") + .instanceLocation(instanceLocation).evaluationPath(getEvaluationPath()) + .arguments(schemaNode.asText()).build(); + throw new InvalidSchemaRefException(validationMessage); } + return refSchema.walk(executionContext, node, rootNode, instanceLocation, shouldValidateSchema); } public JsonSchemaRef getSchemaRef() { return this.schema; } - @Override public void preloadJsonSchema() { JsonSchema jsonSchema = null; @@ -233,14 +219,14 @@ public void preloadJsonSchema() { SchemaLocation schemaLocation = jsonSchema.getSchemaLocation(); JsonSchema check = jsonSchema; boolean circularDependency = false; - while(check.getEvaluationParentSchema() != null) { + while (check.getEvaluationParentSchema() != null) { check = check.getEvaluationParentSchema(); if (check.getSchemaLocation().equals(schemaLocation)) { circularDependency = true; break; } } - if(!circularDependency) { + if (!circularDependency) { jsonSchema.initializeValidators(); } } diff --git a/src/main/java/com/networknt/schema/RequiredValidator.java b/src/main/java/com/networknt/schema/RequiredValidator.java index 369e913f1..131bd42fe 100644 --- a/src/main/java/com/networknt/schema/RequiredValidator.java +++ b/src/main/java/com/networknt/schema/RequiredValidator.java @@ -22,6 +22,9 @@ import java.util.*; +/** + * {@link JsonValidator} for required. + */ public class RequiredValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class); @@ -57,8 +60,9 @@ public Set validate(ExecutionContext executionContext, JsonNo *

* @see Basic */ - errors.add(message().property(fieldName).instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(fieldName).build()); + errors.add(message().instanceNode(node).property(fieldName).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(fieldName).build()); } } diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java index 22f9f9c62..78084a81d 100644 --- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java +++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java @@ -29,6 +29,10 @@ import java.util.Objects; public class SchemaValidatorsConfig { + /** + * Used to validate the acceptable $id values. + */ + private JsonSchemaIdValidator schemaIdValidator = JsonSchemaIdValidator.DEFAULT; /** * when validate type, if TYPE_LOOSE = true, will try to convert string to @@ -545,4 +549,21 @@ public void setFormatAssertionsEnabled(Boolean formatAssertionsEnabled) { this.formatAssertionsEnabled = formatAssertionsEnabled; } + /** + * Gets the schema id validator to validate $id. + * + * @return the validator + */ + public JsonSchemaIdValidator getSchemaIdValidator() { + return schemaIdValidator; + } + + /** + * Sets the schema id validator to validate $id. + * + * @param schemaIdValidator the validator + */ + public void setSchemaIdValidator(JsonSchemaIdValidator schemaIdValidator) { + this.schemaIdValidator = schemaIdValidator; + } } diff --git a/src/main/java/com/networknt/schema/TrueValidator.java b/src/main/java/com/networknt/schema/TrueValidator.java index 908ed483b..0c003d94d 100644 --- a/src/main/java/com/networknt/schema/TrueValidator.java +++ b/src/main/java/com/networknt/schema/TrueValidator.java @@ -22,6 +22,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link JsonValidator} for true. + */ public class TrueValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(TrueValidator.class); diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java index d780c80f2..fb151c9cb 100644 --- a/src/main/java/com/networknt/schema/TypeValidator.java +++ b/src/main/java/com/networknt/schema/TypeValidator.java @@ -23,6 +23,9 @@ import java.util.*; +/** + * {@link JsonValidator} for type. + */ public class TypeValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(TypeValidator.class); @@ -57,21 +60,11 @@ public Set validate(ExecutionContext executionContext, JsonNo if (!equalsToSchemaType(node)) { JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); - return Collections.singleton(message().instanceLocation(instanceLocation) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) .arguments(nodeType.toString(), this.schemaType.toString()).build()); } - - // TODO: Is this really necessary? - // Hack to catch evaluated properties if additionalProperties is given as "additionalProperties":{"type":"string"} - // Hack to catch patternProperties like "^foo":"value" - if (this.schemaLocation.getFragment().getName(-1).equals("type")) { - if (rootNode.isArray()) { - executionContext.getCollectorContext().getEvaluatedItems().add(instanceLocation); - } else if (rootNode.isObject()) { - executionContext.getCollectorContext().getEvaluatedProperties().add(instanceLocation); - } - } return Collections.emptySet(); } } diff --git a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java index 415ff5aa9..b75881b83 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedItemsValidator.java @@ -17,20 +17,35 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; +import static com.networknt.schema.VersionCode.MinV202012; + +/** + * {@link JsonValidator} for unevaluatedItems. + */ public class UnevaluatedItemsValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedItemsValidator.class); private final JsonSchema schema; - public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) { - super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, validationContext); + private final boolean isMinV202012; + private static final VersionFlag DEFAULT_VERSION = VersionFlag.V201909; + public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, + JsonSchema parentSchema, ValidationContext validationContext) { + super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.UNEVALUATED_ITEMS, + validationContext); + isMinV202012 = MinV202012.getVersions().contains(SpecVersionDetector + .detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION)); if (schemaNode.isObject() || schemaNode.isBoolean()) { this.schema = validationContext.newSchema(schemaLocation, evaluationPath, schemaNode, parentSchema); } else { @@ -40,70 +55,156 @@ public UnevaluatedItemsValidator(SchemaLocation schemaLocation, JsonNodePath eva @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isArray()) return Collections.emptySet(); + if (!node.isArray()) { + return Collections.emptySet(); + } debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); - - collectorContext.exitDynamicScope(); - try { - Set allPaths = allPaths(node, instanceLocation); - - // Short-circuit since schema is 'true' - if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { - collectorContext.getEvaluatedItems().addAll(allPaths); - return Collections.emptySet(); + /* + * Keywords renamed in 2020-12 + * + * items -> prefixItems additionalItems -> items + */ + String itemsKeyword = isMinV202012 ? "prefixItems" : "items"; + String additionalItemsKeyword = isMinV202012 ? "items" : "additionalItems"; + + boolean valid = false; + int validCount = 0; + + // This indicates whether the "unevaluatedItems" subschema was used for + // evaluated for setting the annotation + boolean evaluated = false; + + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + List instanceLocationAnnotations = executionContext.getAnnotations().asMap() + .getOrDefault(instanceLocation, Collections.emptyList()); + + // If schema is "unevaluatedItems: true" this is valid + if (getSchemaNode().isBoolean() && getSchemaNode().booleanValue()) { + valid = true; + // No need to actually evaluate since the schema is true but if there are any + // items the annotation needs to be set + if (node.size() > 0) { + evaluated = true; } - - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); - - // Short-circuit since schema is 'false' - if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths, executionContext); + } else { + // Get all the "items" for the instanceLocation + List items = instanceLocationAnnotations.stream() + .filter(a -> itemsKeyword.equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + if (items.isEmpty()) { + // The "items" wasn't applied meaning it is unevaluated if there is content + valid = false; + } else { + // Annotation results for "items" keywords from multiple schemas applied to the + // same instance location are combined by setting the combined result to true if + // any of the values are true, and otherwise retaining the largest numerical + // value. + for (JsonNodeAnnotation annotation : items) { + if (annotation.getValue() instanceof Number) { + Number value = annotation.getValue(); + int existing = value.intValue(); + if (existing > validCount) { + validCount = existing; + } + } else if (annotation.getValue() instanceof Boolean) { + // The annotation "items: true" + valid = true; + } + } } - - Set failingPaths = new LinkedHashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = path.getPathType().convertToJsonPointer(path.toString()); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { - failingPaths.add(path); + if (!valid) { + // Check the additionalItems annotation + // If the "additionalItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "additionalItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + List additionalItems = instanceLocationAnnotations.stream() + .filter(a -> additionalItemsKeyword.equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalItems) { + if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "additionalItems: true" + valid = true; + } + } + } + if (!valid) { + // Unevaluated + // Check if there are any "unevaluatedItems" annotations + List unevaluatedItems = instanceLocationAnnotations.stream() + .filter(a -> "unevaluatedItems".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedItems) { + if (annotation.getValue() instanceof Boolean && Boolean.TRUE.equals(annotation.getValue())) { + // The annotation "unevaluatedItems: true" + valid = true; + } } - }); + } + } + Set messages = null; + if (!valid) { + // Get all the "contains" for the instanceLocation + List contains = instanceLocationAnnotations.stream() + .filter(a -> "contains".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + + Set containsEvaluated = new HashSet<>(); + boolean containsEvaluatedAll = false; + for (JsonNodeAnnotation a : contains) { + if (a.getValue() instanceof List) { + List values = a.getValue(); + containsEvaluated.addAll(values); + } else if (a.getValue() instanceof Boolean) { + containsEvaluatedAll = true; + } + } - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedItems().addAll(allPaths); + messages = new LinkedHashSet<>(); + if (!containsEvaluatedAll) { + // Start evaluating from the valid count + for (int x = validCount; x < node.size(); x++) { + // The schema is either "false" or an object schema + if (!containsEvaluated.contains(x)) { + messages.addAll( + this.schema.validate(executionContext, node.get(x), node, instanceLocation.append(x))); + evaluated = true; + } + } + } + if (messages.isEmpty()) { + valid = true; } else { - return reportUnevaluatedPaths(failingPaths, executionContext); + // Report these as unevaluated paths or not matching the unevalutedItems schema + messages = messages.stream() + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); } - - return Collections.emptySet(); - } finally { - collectorContext.enterDynamicScope(); } - } - - private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { - Set collector = new LinkedHashSet<>(); - int size = node.size(); - for (int i = 0; i < size; ++i) { - JsonNodePath path = instanceLocation.append(i); - collector.add(path); + // If the "unevaluatedItems" subschema is applied to any positions within the + // instance array, it produces an annotation result of boolean true, analogous + // to the single schema behavior of "items". If any "unevaluatedItems" keyword + // from any subschema applied to the same instance location produces an + // annotation value of true, then the combined result from these keywords is + // also true. + if (evaluated) { + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation) + .evaluationPath(this.evaluationPath).schemaLocation(this.schemaLocation) + .keyword("unevaluatedItems").value(true).build()); } - return collector; - } - - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - return unevaluatedPaths - .stream().map(path -> message().instanceLocation(path) - .locale(executionContext.getExecutionConfig().getLocale()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; } - - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new HashSet<>(allPaths); - unevaluatedProperties.removeAll(collectorContext.getEvaluatedItems()); - return unevaluatedProperties; - } - } diff --git a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java index ddd676cd8..441afa3ad 100644 --- a/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java +++ b/src/main/java/com/networknt/schema/UnevaluatedPropertiesValidator.java @@ -17,12 +17,18 @@ package com.networknt.schema; import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.annotation.JsonNodeAnnotation; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.*; +import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * {@link JsonValidator} for unevaluatedProperties. + */ public class UnevaluatedPropertiesValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnevaluatedPropertiesValidator.class); @@ -40,67 +46,97 @@ public UnevaluatedPropertiesValidator(SchemaLocation schemaLocation, JsonNodePat @Override public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { - if (!executionContext.getExecutionConfig().getAnnotationAllowedPredicate().test(getKeyword()) || !node.isObject()) return Collections.emptySet(); + if (!node.isObject()) { + return Collections.emptySet(); + } debug(logger, node, rootNode, instanceLocation); - CollectorContext collectorContext = executionContext.getCollectorContext(); - - collectorContext.exitDynamicScope(); - try { - Set allPaths = allPaths(node, instanceLocation); + // Get all the valid adjacent annotations + Predicate validEvaluationPathFilter = a -> { + return executionContext.getResults().isValid(instanceLocation, a.getEvaluationPath()); + }; + + Predicate adjacentEvaluationPathFilter = a -> a.getEvaluationPath() + .startsWith(this.evaluationPath.getParent()); + + List instanceLocationAnnotations = executionContext.getAnnotations().asMap() + .getOrDefault(instanceLocation, Collections.emptyList()); + + Set evaluatedProperties = new LinkedHashSet<>(); // The properties that unevaluatedProperties schema + Set existingEvaluatedProperties = new LinkedHashSet<>(); + // Get all the "properties" for the instanceLocation + List properties = instanceLocationAnnotations.stream() + .filter(a -> "properties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : properties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } - // Short-circuit since schema is 'true' - if (super.schemaNode.isBoolean() && super.schemaNode.asBoolean()) { - collectorContext.getEvaluatedProperties().addAll(allPaths); - return Collections.emptySet(); + // Get all the "patternProperties" for the instanceLocation + List patternProperties = instanceLocationAnnotations.stream() + .filter(a -> "patternProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : patternProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } + } - Set unevaluatedPaths = unevaluatedPaths(collectorContext, allPaths); + // Get all the "patternProperties" for the instanceLocation + List additionalProperties = instanceLocationAnnotations.stream() + .filter(a -> "additionalProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : additionalProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); + } + } - // Short-circuit since schema is 'false' - if (super.schemaNode.isBoolean() && !super.schemaNode.asBoolean() && !unevaluatedPaths.isEmpty()) { - return reportUnevaluatedPaths(unevaluatedPaths, executionContext); + // Get all the "unevaluatedProperties" for the instanceLocation + List unevaluatedProperties = instanceLocationAnnotations.stream() + .filter(a -> "unevaluatedProperties".equals(a.getKeyword())).filter(adjacentEvaluationPathFilter) + .filter(validEvaluationPathFilter).collect(Collectors.toList()); + for (JsonNodeAnnotation annotation : unevaluatedProperties) { + if (annotation.getValue() instanceof Set) { + Set p = annotation.getValue(); + existingEvaluatedProperties.addAll(p); } + } - Set failingPaths = new LinkedHashSet<>(); - unevaluatedPaths.forEach(path -> { - String pointer = path.getPathType().convertToJsonPointer(path.toString()); - JsonNode property = rootNode.at(pointer); - if (!this.schema.validate(executionContext, property, rootNode, path).isEmpty()) { - failingPaths.add(path); + Set messages = new LinkedHashSet<>(); + for (Iterator it = node.fieldNames(); it.hasNext();) { + String fieldName = it.next(); + if (!existingEvaluatedProperties.contains(fieldName)) { + evaluatedProperties.add(fieldName); + if (this.schemaNode.isBoolean() && this.schemaNode.booleanValue() == false) { + // All fails as "unevaluatedProperties: false" + messages.add(message().instanceNode(node).instanceLocation(instanceLocation.append(fieldName)) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); + } else { + messages.addAll(this.schema.validate(executionContext, node.get(fieldName), node, + instanceLocation.append(fieldName))); } - }); - - if (failingPaths.isEmpty()) { - collectorContext.getEvaluatedProperties().addAll(allPaths); - } else { - return reportUnevaluatedPaths(failingPaths, executionContext); } - - return Collections.emptySet(); - } finally { - collectorContext.enterDynamicScope(); } - } - - private Set allPaths(JsonNode node, JsonNodePath instanceLocation) { - Set collector = new LinkedHashSet<>(); - node.fields().forEachRemaining(entry -> { - collector.add(instanceLocation.append(entry.getKey())); - }); - return collector; - } - - private Set reportUnevaluatedPaths(Set unevaluatedPaths, ExecutionContext executionContext) { - return unevaluatedPaths - .stream().map(path -> message().instanceLocation(path) - .locale(executionContext.getExecutionConfig().getLocale()).build()) - .collect(Collectors.toCollection(LinkedHashSet::new)); - } + if (!messages.isEmpty()) { + // Report these as unevaluated paths or not matching the unevaluatedProperties + // schema + messages = messages.stream() + .map(m -> message().instanceNode(node).instanceLocation(m.getInstanceLocation()) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } + executionContext.getAnnotations() + .put(JsonNodeAnnotation.builder().instanceLocation(instanceLocation).evaluationPath(this.evaluationPath) + .schemaLocation(this.schemaLocation).keyword(getKeyword()).value(evaluatedProperties).build()); - private static Set unevaluatedPaths(CollectorContext collectorContext, Set allPaths) { - Set unevaluatedProperties = new LinkedHashSet<>(allPaths); - unevaluatedProperties.removeAll(collectorContext.getEvaluatedProperties()); - return unevaluatedProperties; + return messages == null || messages.isEmpty() ? Collections.emptySet() : messages; } } diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java index e23b316ca..c04626a08 100644 --- a/src/main/java/com/networknt/schema/UnionTypeValidator.java +++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java @@ -25,6 +25,9 @@ import java.util.List; import java.util.Set; +/** + * {@link JsonValidator} for type union. + */ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UnionTypeValidator.class); @@ -69,17 +72,27 @@ public Set validate(ExecutionContext executionContext, JsonNo boolean valid = false; - for (JsonValidator schema : schemas) { - Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); - if (errors == null || errors.isEmpty()) { - valid = true; - break; + // Save flag as nested schema evaluation shouldn't trigger fail fast + boolean failFast = executionContext.getExecutionConfig().isFailFast(); + try { + executionContext.getExecutionConfig().setFailFast(false); + for (JsonValidator schema : schemas) { + Set errors = schema.validate(executionContext, node, rootNode, instanceLocation); + if (errors == null || errors.isEmpty()) { + valid = true; + break; + } } + } finally { + // Restore flag + executionContext.getExecutionConfig().setFailFast(failFast); } if (!valid) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(nodeType.toString(), error) + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .type("type") + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).arguments(nodeType.toString(), error) .build()); } diff --git a/src/main/java/com/networknt/schema/UniqueItemsValidator.java b/src/main/java/com/networknt/schema/UniqueItemsValidator.java index 1c7655246..dd8c5c6e0 100644 --- a/src/main/java/com/networknt/schema/UniqueItemsValidator.java +++ b/src/main/java/com/networknt/schema/UniqueItemsValidator.java @@ -24,6 +24,9 @@ import java.util.HashSet; import java.util.Set; +/** + * {@link JsonValidator} for uniqueItems. + */ public class UniqueItemsValidator extends BaseJsonValidator implements JsonValidator { private static final Logger logger = LoggerFactory.getLogger(UniqueItemsValidator.class); @@ -43,8 +46,9 @@ public Set validate(ExecutionContext executionContext, JsonNo Set set = new HashSet(); for (JsonNode n : node) { if (!set.add(n)) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } } } diff --git a/src/main/java/com/networknt/schema/ValidationMessage.java b/src/main/java/com/networknt/schema/ValidationMessage.java index 72d1df861..0bb24160d 100644 --- a/src/main/java/com/networknt/schema/ValidationMessage.java +++ b/src/main/java/com/networknt/schema/ValidationMessage.java @@ -16,6 +16,13 @@ package com.networknt.schema; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; import com.networknt.schema.i18n.MessageFormatter; import com.networknt.schema.utils.CachingSupplier; import com.networknt.schema.utils.StringUtils; @@ -32,21 +39,30 @@ * "https://github.com/json-schema-org/json-schema-spec/blob/main/jsonschema-validation-output-machines.md">JSON * Schema */ +@JsonIgnoreProperties({ "messageSupplier", "schemaNode", "instanceNode", "valid" }) +@JsonPropertyOrder({ "type", "code", "message", "instanceLocation", "property", "evaluationPath", "schemaLocation", + "messageKey", "arguments", "details" }) +@JsonInclude(Include.NON_NULL) public class ValidationMessage { private final String type; private final String code; + @JsonSerialize(using = ToStringSerializer.class) private final JsonNodePath evaluationPath; + @JsonSerialize(using = ToStringSerializer.class) private final SchemaLocation schemaLocation; + @JsonSerialize(using = ToStringSerializer.class) private final JsonNodePath instanceLocation; private final String property; private final Object[] arguments; - private final Map details; private final String messageKey; private final Supplier messageSupplier; + private final Map details; + private final JsonNode instanceNode; + private final JsonNode schemaNode; ValidationMessage(String type, String code, JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNodePath instanceLocation, String property, Object[] arguments, Map details, - String messageKey, Supplier messageSupplier) { + String messageKey, Supplier messageSupplier, JsonNode instanceNode, JsonNode schemaNode) { super(); this.type = type; this.code = code; @@ -58,6 +74,8 @@ public class ValidationMessage { this.details = details; this.messageKey = messageKey; this.messageSupplier = messageSupplier; + this.instanceNode = instanceNode; + this.schemaNode = schemaNode; } public String getCode() { @@ -97,6 +115,37 @@ public SchemaLocation getSchemaLocation() { return schemaLocation; } + /** + * Returns the instance node which was evaluated. + *

+ * This corresponds with the instance location. + * + * @return the instance node + */ + public JsonNode getInstanceNode() { + return instanceNode; + } + + /** + * Returns the schema node which was evaluated. + *

+ * This corresponds with the schema location. + * + * @return the schema node + */ + public JsonNode getSchemaNode() { + return schemaNode; + } + + /** + * Returns the property with the error. + *

+ * For instance, for the required validator the instance location does not + * contain the missing property name as the instance must refer to the input + * data. + * + * @return the property name + */ public String getProperty() { return property; } @@ -186,6 +235,8 @@ public static abstract class BuilderSupport { protected Supplier messageSupplier; protected MessageFormatter messageFormatter; protected String messageKey; + protected JsonNode instanceNode; + protected JsonNode schemaNode; public S type(String type) { this.type = type; @@ -288,6 +339,16 @@ public S messageKey(String messageKey) { this.messageKey = messageKey; return self(); } + + public S instanceNode(JsonNode instanceNode) { + this.instanceNode = instanceNode; + return self(); + } + + public S schemaNode(JsonNode schemaNode) { + this.schemaNode = schemaNode; + return self(); + } public ValidationMessage build() { Supplier messageSupplier = this.messageSupplier; @@ -308,7 +369,7 @@ public ValidationMessage build() { messageSupplier = new CachingSupplier<>(() -> formatter.format(objs)); } return new ValidationMessage(type, code, evaluationPath, schemaLocation, instanceLocation, - property, arguments, details, messageKey, messageSupplier); + property, arguments, details, messageKey, messageSupplier, this.instanceNode, this.schemaNode); } protected Object[] getMessageArguments() { diff --git a/src/main/java/com/networknt/schema/ValidationMessageHandler.java b/src/main/java/com/networknt/schema/ValidationMessageHandler.java index 663c036c2..1bacaffdb 100644 --- a/src/main/java/com/networknt/schema/ValidationMessageHandler.java +++ b/src/main/java/com/networknt/schema/ValidationMessageHandler.java @@ -10,7 +10,6 @@ import java.util.Objects; public abstract class ValidationMessageHandler { - protected boolean failFast; protected final MessageSource messageSource; protected ErrorMessageType errorMessageType; @@ -25,10 +24,9 @@ public abstract class ValidationMessageHandler { protected Keyword keyword; - protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessageType, - boolean customErrorMessagesEnabled, MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, - SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - this.failFast = failFast; + protected ValidationMessageHandler(ErrorMessageType errorMessageType, boolean customErrorMessagesEnabled, + MessageSource messageSource, Keyword keyword, JsonSchema parentSchema, SchemaLocation schemaLocation, + JsonNodePath evaluationPath) { this.errorMessageType = errorMessageType; this.messageSource = messageSource; this.schemaLocation = Objects.requireNonNull(schemaLocation); @@ -44,7 +42,6 @@ protected ValidationMessageHandler(boolean failFast, ErrorMessageType errorMessa * @param copy to copy from */ protected ValidationMessageHandler(ValidationMessageHandler copy) { - this.failFast = copy.failFast; this.messageSource = copy.messageSource; this.errorMessageType = copy.errorMessageType; this.schemaLocation = copy.schemaLocation; @@ -57,9 +54,9 @@ protected ValidationMessageHandler(ValidationMessageHandler copy) { } protected MessageSourceValidationMessage.Builder message() { - return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, message -> { - if (this.failFast && isApplicator()) { - throw new JsonSchemaException(message); + return MessageSourceValidationMessage.builder(this.messageSource, this.errorMessage, (message, failFast) -> { + if (failFast) { + throw new FailFastAssertionException(message); } }).code(getErrorMessageType().getErrorCode()).schemaLocation(this.schemaLocation) .evaluationPath(this.evaluationPath).type(this.keyword != null ? this.keyword.getValue() : null) @@ -70,42 +67,6 @@ protected ErrorMessageType getErrorMessageType() { return this.errorMessageType; } - private boolean isApplicator() { - return !isPartOfAnyOfMultipleType() - && !isPartOfIfMultipleType() - && !isPartOfNotMultipleType() - && !isPartOfOneOfMultipleType(); - } - - private boolean isPartOfAnyOfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.ANY_OF.getValue()); - } - - private boolean isPartOfIfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.IF_THEN_ELSE.getValue()); - } - - private boolean isPartOfNotMultipleType() { - return schemaLocationContains(ValidatorTypeCode.NOT.getValue()); - } - - protected boolean schemaLocationContains(String match) { - int count = this.parentSchema.schemaLocation.getFragment().getNameCount(); - for (int x = 0; x < count; x++) { - String name = this.parentSchema.schemaLocation.getFragment().getName(x); - if (match.equals(name)) { - return true; - } - } - return false; - } - - /* ********************** START OF OpenAPI 3.0.x DISCRIMINATOR METHODS ********************************* */ - - protected boolean isPartOfOneOfMultipleType() { - return schemaLocationContains(ValidatorTypeCode.ONE_OF.getValue()); - } - protected void parseErrorCode(String errorCodeKey) { if (errorCodeKey != null && this.parentSchema != null) { JsonNode errorCodeNode = this.parentSchema.getSchemaNode().get(errorCodeKey); @@ -180,7 +141,7 @@ protected JsonNode getMessageNode(JsonNode schemaNode, JsonSchema parentSchema, } return messageNode; } - + protected String getErrorCodeKey(String keyword) { if (keyword != null) { return keyword + "ErrorCode"; diff --git a/src/main/java/com/networknt/schema/Version201909.java b/src/main/java/com/networknt/schema/Version201909.java index 81db41595..0972bdcb0 100644 --- a/src/main/java/com/networknt/schema/Version201909.java +++ b/src/main/java/com/networknt/schema/Version201909.java @@ -33,17 +33,17 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V201909)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$recursiveAnchor"), - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$vocabulary"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$recursiveAnchor", false), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$vocabulary", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs"), // newly added in 2019-09 release. - new NonValidationKeyword("$anchor"), + new NonValidationKeyword("$defs", false), // newly added in 2019-09 release. + new NonValidationKeyword("$anchor", false), new NonValidationKeyword("additionalItems"), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), diff --git a/src/main/java/com/networknt/schema/Version202012.java b/src/main/java/com/networknt/schema/Version202012.java index 581278369..4ff920741 100644 --- a/src/main/java/com/networknt/schema/Version202012.java +++ b/src/main/java/com/networknt/schema/Version202012.java @@ -35,16 +35,17 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V202012)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), - new NonValidationKeyword("$defs"), - new NonValidationKeyword("$anchor"), - new NonValidationKeyword("$dynamicAnchor"), + new NonValidationKeyword("$defs", false), + new NonValidationKeyword("$anchor", false), + new NonValidationKeyword("$dynamicAnchor", false), + new NonValidationKeyword("$vocabulary", false), new NonValidationKeyword("deprecated"), new NonValidationKeyword("contentMediaType"), new NonValidationKeyword("contentEncoding"), diff --git a/src/main/java/com/networknt/schema/Version4.java b/src/main/java/com/networknt/schema/Version4.java index 8dd86aa79..b9f886599 100644 --- a/src/main/java/com/networknt/schema/Version4.java +++ b/src/main/java/com/networknt/schema/Version4.java @@ -19,12 +19,12 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V4)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("additionalItems"), new NonValidationKeyword("exampleSetFlag") )) diff --git a/src/main/java/com/networknt/schema/Version6.java b/src/main/java/com/networknt/schema/Version6.java index 920b9520f..1f92e37eb 100644 --- a/src/main/java/com/networknt/schema/Version6.java +++ b/src/main/java/com/networknt/schema/Version6.java @@ -20,13 +20,13 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V6)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("definitions") + new NonValidationKeyword("definitions", false) )) .build(); } diff --git a/src/main/java/com/networknt/schema/Version7.java b/src/main/java/com/networknt/schema/Version7.java index ebf01d849..2558b343e 100644 --- a/src/main/java/com/networknt/schema/Version7.java +++ b/src/main/java/com/networknt/schema/Version7.java @@ -19,18 +19,18 @@ public JsonMetaSchema getInstance() { .addKeywords(ValidatorTypeCode.getNonFormatKeywords(SpecVersion.VersionFlag.V7)) // keywords that may validly exist, but have no validation aspect to them .addKeywords(Arrays.asList( - new NonValidationKeyword("$schema"), - new NonValidationKeyword("$id"), + new NonValidationKeyword("$schema", false), + new NonValidationKeyword("$id", false), new NonValidationKeyword("title"), new NonValidationKeyword("description"), new NonValidationKeyword("default"), - new NonValidationKeyword("definitions"), + new NonValidationKeyword("definitions", false), new NonValidationKeyword("$comment"), new NonValidationKeyword("examples"), new NonValidationKeyword("then"), new NonValidationKeyword("else"), new NonValidationKeyword("additionalItems"), - new NonValidationKeyword("message") + new NonValidationKeyword("message", false) )) .build(); } diff --git a/src/main/java/com/networknt/schema/WriteOnlyValidator.java b/src/main/java/com/networknt/schema/WriteOnlyValidator.java index 71171920c..ed2ce4ae9 100644 --- a/src/main/java/com/networknt/schema/WriteOnlyValidator.java +++ b/src/main/java/com/networknt/schema/WriteOnlyValidator.java @@ -8,6 +8,9 @@ import com.fasterxml.jackson.databind.JsonNode; +/** + * {@link JsonValidator} for writeOnly. + */ public class WriteOnlyValidator extends BaseJsonValidator { private static final Logger logger = LoggerFactory.getLogger(WriteOnlyValidator.class); @@ -24,8 +27,9 @@ public WriteOnlyValidator(SchemaLocation schemaLocation, JsonNodePath evaluation public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); if (this.writeOnly) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()).build()); } return Collections.emptySet(); } diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java new file mode 100644 index 000000000..086bc35ee --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotation.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023 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.annotation; + +import java.util.Objects; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.Keyword; +import com.networknt.schema.SchemaLocation; + +/** + * The annotation. + */ +public class JsonNodeAnnotation { + private final String keyword; + private final JsonNodePath instanceLocation; + private final SchemaLocation schemaLocation; + private final JsonNodePath evaluationPath; + private final Object value; + + public JsonNodeAnnotation(String keyword, JsonNodePath instanceLocation, SchemaLocation schemaLocation, + JsonNodePath evaluationPath, Object value) { + super(); + this.keyword = keyword; + this.instanceLocation = instanceLocation; + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.value = value; + } + + /** + * The keyword that produces the annotation. + * + * @return the keyword + */ + public String getKeyword() { + return keyword; + } + + /** + * The instance location to which it is attached, as a JSON Pointer. + * + * @return the instance location + */ + public JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + /** + * The schema location of the attaching keyword, as a IRI and JSON Pointer + * fragment. + * + * @return the schema location + */ + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + /** + * The evaluation path, indicating how reference keywords such as "$ref" were + * followed to reach the absolute schema location. + * + * @return the evaluation path + */ + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + /** + * The attached value(s). + * + * @param the value type + * @return the value + */ + @SuppressWarnings("unchecked") + public T getValue() { + return (T) value; + } + + @Override + public String toString() { + return "JsonNodeAnnotation [evaluationPath=" + evaluationPath + ", schemaLocation=" + schemaLocation + + ", instanceLocation=" + instanceLocation + ", keyword=" + keyword + ", value=" + value + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, keyword, schemaLocation, value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonNodeAnnotation other = (JsonNodeAnnotation) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) && Objects.equals(keyword, other.keyword) + && Objects.equals(schemaLocation, other.schemaLocation) && Objects.equals(value, other.value); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String keyword; + private JsonNodePath instanceLocation; + private SchemaLocation schemaLocation; + private JsonNodePath evaluationPath; + private Object value; + + public Builder keyword(Keyword keyword) { + this.keyword = keyword.getValue(); + return this; + } + + public Builder keyword(String keyword) { + this.keyword = keyword; + return this; + } + + public Builder instanceLocation(JsonNodePath instanceLocation) { + this.instanceLocation = instanceLocation; + return this; + } + + public Builder schemaLocation(SchemaLocation schemaLocation) { + this.schemaLocation = schemaLocation; + return this; + } + + public Builder evaluationPath(JsonNodePath evaluationPath) { + this.evaluationPath = evaluationPath; + return this; + } + + public Builder value(Object value) { + this.value = value; + return this; + } + + public JsonNodeAnnotation build() { + return new JsonNodeAnnotation(keyword, instanceLocation, schemaLocation, evaluationPath, value); + } + } + +} diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java new file mode 100644 index 000000000..5f356af12 --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotationPredicate.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 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.annotation; + +import java.util.function.Predicate; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * A predicate for filtering annotations. + */ +public class JsonNodeAnnotationPredicate implements Predicate { + final Predicate instanceLocationPredicate; + final Predicate evaluationPathPredicate; + final Predicate schemaLocationPredicate; + final Predicate keywordPredicate; + final Predicate valuePredicate; + + /** + * Initialize a new instance of this class. + * + * @param instanceLocationPredicate for instanceLocation + * @param evaluationPathPredicate for evaluationPath + * @param schemaLocationPredicate for schemaLocation + * @param keywordPredicate for keyword + * @param valuePredicate for value + */ + protected JsonNodeAnnotationPredicate(Predicate instanceLocationPredicate, + Predicate evaluationPathPredicate, Predicate schemaLocationPredicate, + Predicate keywordPredicate, Predicate valuePredicate) { + super(); + this.instanceLocationPredicate = instanceLocationPredicate; + this.evaluationPathPredicate = evaluationPathPredicate; + this.schemaLocationPredicate = schemaLocationPredicate; + this.keywordPredicate = keywordPredicate; + this.valuePredicate = valuePredicate; + } + + @Override + public boolean test(JsonNodeAnnotation t) { + return ((valuePredicate == null || valuePredicate.test(t.getValue())) + && (keywordPredicate == null || keywordPredicate.test(t.getKeyword())) + && (instanceLocationPredicate == null || instanceLocationPredicate.test(t.getInstanceLocation())) + && (evaluationPathPredicate == null || evaluationPathPredicate.test(t.getEvaluationPath())) + && (schemaLocationPredicate == null || schemaLocationPredicate.test(t.getSchemaLocation()))); + } + + /** + * Gets the predicate to filter on instanceLocation. + * + * @return the predicate + */ + public Predicate getInstanceLocationPredicate() { + return instanceLocationPredicate; + } + + /** + * Gets the predicate to filter on evaluationPath. + * + * @return the predicate + */ + public Predicate getEvaluationPathPredicate() { + return evaluationPathPredicate; + } + + /** + * Gets the predicate to filter on schemaLocation. + * + * @return the predicate + */ + public Predicate getSchemaLocationPredicate() { + return schemaLocationPredicate; + } + + /** + * Gets the predicate to filter on keyword. + * + * @return the predicate + */ + public Predicate getKeywordPredicate() { + return keywordPredicate; + } + + /** + * Gets the predicate to filter on value. + * + * @return the predicate + */ + public Predicate getValuePredicate() { + return valuePredicate; + } + + /** + * Creates a new builder to create the predicate. + * + * @return the builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for building a {@link JsonNodeAnnotationPredicate}. + */ + public static class Builder { + Predicate instanceLocationPredicate; + Predicate evaluationPathPredicate; + Predicate schemaLocationPredicate; + Predicate keywordPredicate; + Predicate valuePredicate; + + public Builder instanceLocation(Predicate instanceLocationPredicate) { + this.instanceLocationPredicate = instanceLocationPredicate; + return this; + } + + public Builder evaluationPath(Predicate evaluationPathPredicate) { + this.evaluationPathPredicate = evaluationPathPredicate; + return this; + } + + public Builder schema(Predicate schemaLocationPredicate) { + this.schemaLocationPredicate = schemaLocationPredicate; + return this; + } + + public Builder keyword(Predicate keywordPredicate) { + this.keywordPredicate = keywordPredicate; + return this; + } + + public Builder value(Predicate valuePredicate) { + this.valuePredicate = valuePredicate; + return this; + } + + public JsonNodeAnnotationPredicate build() { + return new JsonNodeAnnotationPredicate(instanceLocationPredicate, evaluationPathPredicate, + schemaLocationPredicate, keywordPredicate, valuePredicate); + } + } +} diff --git a/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java new file mode 100644 index 000000000..4e4f58278 --- /dev/null +++ b/src/main/java/com/networknt/schema/annotation/JsonNodeAnnotations.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023 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.annotation; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * The JSON Schema annotations. + * + * @see Details + * of annotation collection + */ +public class JsonNodeAnnotations { + + /** + * Stores the annotations. + *

+ * instancePath to annotation + */ + private final Map> values = new LinkedHashMap<>(); + + /** + * Gets the annotations. + *

+ * instancePath to annotation + * + * @return the annotations + */ + public Map> asMap() { + return this.values; + } + + /** + * Puts the annotation. + * + * @param annotation the annotation + */ + public void put(JsonNodeAnnotation annotation) { + this.values.computeIfAbsent(annotation.getInstanceLocation(), (k) -> new ArrayList<>()).add(annotation); + + } + + @Override + public String toString() { + return Formatter.format(this.values); + } + + /** + * Formatter for pretty printing the annotations. + */ + public static class Formatter { + /** + * Formats the annotations. + * + * @param annotations the annotations + * @return the formatted JSON + */ + public static String format(Map> annotations) { + Map>> results = new LinkedHashMap<>(); + for (List list : annotations.values()) { + for (JsonNodeAnnotation annotation : list) { + String keyword = annotation.getKeyword(); + String instancePath = annotation.getInstanceLocation().toString(); + String evaluationPath = annotation.getEvaluationPath().toString(); + Map values = results + .computeIfAbsent(instancePath, (key) -> new LinkedHashMap<>()) + .computeIfAbsent(keyword, (key) -> new LinkedHashMap<>()); + values.put(evaluationPath, annotation.getValue()); + } + } + + try { + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(results); + } catch (JsonProcessingException e) { + return ""; + } + } + + } + +} diff --git a/src/main/java/com/networknt/schema/format/AbstractFormat.java b/src/main/java/com/networknt/schema/format/AbstractFormat.java index d0b9f270b..52f7fff37 100644 --- a/src/main/java/com/networknt/schema/format/AbstractFormat.java +++ b/src/main/java/com/networknt/schema/format/AbstractFormat.java @@ -22,6 +22,12 @@ * Used for Formats that do not need to use the {@link ExecutionContext}. */ public abstract class AbstractFormat extends BaseFormat { + /** + * Constructor. + * + * @param name the name + * @param errorMessageDescription the error message description + */ public AbstractFormat(String name, String errorMessageDescription) { super(name, errorMessageDescription); } diff --git a/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java b/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java index c839bd3e3..9b68b9962 100644 --- a/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java +++ b/src/main/java/com/networknt/schema/format/AbstractRFC3986Format.java @@ -3,8 +3,17 @@ import java.net.URI; import java.net.URISyntaxException; +/** + * {@link AbstractFormat} for RFC 3986. + */ public abstract class AbstractRFC3986Format extends AbstractFormat { + /** + * Constructor. + * + * @param name the format name + * @param errorMessageDescription the error message description + */ public AbstractRFC3986Format(String name, String errorMessageDescription) { super(name, errorMessageDescription); } @@ -19,6 +28,12 @@ public final boolean matches(String value) { } } + /** + * Determines if the uri matches the format. + * + * @param uri the uri to match + * @return true if matches + */ protected abstract boolean validate(URI uri); } diff --git a/src/main/java/com/networknt/schema/format/DateTimeValidator.java b/src/main/java/com/networknt/schema/format/DateTimeValidator.java index 5e062b2d8..164f0efb9 100644 --- a/src/main/java/com/networknt/schema/format/DateTimeValidator.java +++ b/src/main/java/com/networknt/schema/format/DateTimeValidator.java @@ -35,6 +35,9 @@ import java.util.Collections; import java.util.Set; +/** + * {@link BaseFormatJsonValidator} for format for date-time. + */ public class DateTimeValidator extends BaseFormatJsonValidator { private static final Logger logger = LoggerFactory.getLogger(DateTimeValidator.class); private static final String DATETIME = "date-time"; @@ -47,17 +50,25 @@ public DateTimeValidator(SchemaLocation schemaLocation, JsonNodePath evaluationP public Set validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) { debug(logger, node, rootNode, instanceLocation); + if (collectAnnotations(executionContext, "format")) { + putAnnotation(executionContext, + annotation -> annotation.instanceLocation(instanceLocation).keyword("format").value(DATETIME)); + } + JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig()); if (nodeType != JsonType.STRING) { return Collections.emptySet(); } + boolean assertionsEnabled = isAssertionsEnabled(executionContext); if (!isLegalDateTime(node.textValue())) { if (assertionsEnabled) { - return Collections.singleton(message().instanceLocation(instanceLocation) - .locale(executionContext.getExecutionConfig().getLocale()).arguments(node.textValue(), DATETIME) - .build()); + return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) + .type("format") + .locale(executionContext.getExecutionConfig().getLocale()) + .failFast(executionContext.getExecutionConfig().isFailFast()) + .arguments(node.textValue(), DATETIME).build()); } } return Collections.emptySet(); diff --git a/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java index 79acb5b53..e62a73c5a 100644 --- a/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java +++ b/src/main/java/com/networknt/schema/i18n/DefaultMessageSource.java @@ -19,8 +19,14 @@ * The default {@link MessageSource} singleton. */ public class DefaultMessageSource { + /** + * The bundle base name. + */ public static final String BUNDLE_BASE_NAME = "jsv-messages"; + /** + * The holder. + */ public static class Holder { private static final MessageSource INSTANCE = new ResourceBundleMessageSource(BUNDLE_BASE_NAME); } diff --git a/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java new file mode 100644 index 000000000..51950baec --- /dev/null +++ b/src/main/java/com/networknt/schema/output/HierarchicalOutputUnitFormatter.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2024 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.output; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.ValidationContext; +import com.networknt.schema.ValidationMessage; + +/** + * HierarchicalOutputUnitFormatter. + */ +public class HierarchicalOutputUnitFormatter { + public static OutputUnit format(JsonSchema jsonSchema, Set validationMessages, + ExecutionContext executionContext, ValidationContext validationContext) { + + OutputUnit root = new OutputUnit(); + root.setValid(validationMessages.isEmpty()); + + root.setInstanceLocation(validationContext.getConfig().getPathType().getRoot()); + root.setEvaluationPath(validationContext.getConfig().getPathType().getRoot()); + root.setSchemaLocation(jsonSchema.getSchemaLocation().toString()); + + OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); + + Map valid = data.getValid(); + Map> errors = data.getErrors(); + Map> annotations = data.getAnnotations(); + Map> droppedAnnotations = data.getDroppedAnnotations(); + + // Evaluation path to output unit + Map index = new LinkedHashMap<>(); + index.put(new JsonNodePath(validationContext.getConfig().getPathType()), root); + + // Get all the evaluation paths with data + Set keys = new LinkedHashSet<>(); + errors.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + annotations.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + droppedAnnotations.keySet().stream().forEach(k -> keys.add(k.getEvaluationPath())); + + errors.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + annotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + droppedAnnotations.keySet().stream().forEach(k -> buildIndex(k, index, keys, root)); + + // Process all the data + for (Entry> error : errors.entrySet()) { + OutputUnitKey key = error.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + unit.setInstanceLocation(key.getInstanceLocation().toString()); + unit.setSchemaLocation(key.getSchemaLocation().toString()); + unit.setValid(false); + unit.setErrors(error.getValue()); + } + + for (Entry> annotation : annotations.entrySet()) { + OutputUnitKey key = annotation.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + String instanceLocation = key.getInstanceLocation().toString(); + String schemaLocation = key.getSchemaLocation().toString(); + if (unit.getInstanceLocation() != null && !unit.getInstanceLocation().equals(instanceLocation)) { + throw new IllegalArgumentException(); + } + if (unit.getSchemaLocation() != null && !unit.getSchemaLocation().equals(schemaLocation)) { + throw new IllegalArgumentException(); + } + unit.setInstanceLocation(instanceLocation); + unit.setSchemaLocation(schemaLocation); + unit.setAnnotations(annotation.getValue()); + unit.setValid(valid.get(key)); + } + + for (Entry> droppedAnnotation : droppedAnnotations.entrySet()) { + OutputUnitKey key = droppedAnnotation.getKey(); + OutputUnit unit = index.get(key.getEvaluationPath()); + String instanceLocation = key.getInstanceLocation().toString(); + String schemaLocation = key.getSchemaLocation().toString(); + if (unit.getInstanceLocation() != null && !unit.getInstanceLocation().equals(instanceLocation)) { + throw new IllegalArgumentException(); + } + if (unit.getSchemaLocation() != null && !unit.getSchemaLocation().equals(schemaLocation)) { + throw new IllegalArgumentException(); + } + unit.setInstanceLocation(instanceLocation); + unit.setSchemaLocation(schemaLocation); + unit.setDroppedAnnotations(droppedAnnotation.getValue()); + unit.setValid(valid.get(key)); + } + return root; + } + + /** + * Builds in the index of evaluation path to output units to be populated later + * and modify the root to add the appropriate children. + * + * @param key the current key to process + * @param index contains all the mappings from evaluation path to output units + * @param keys that contain all the evaluation paths with data + * @param root the root output unit + */ + protected static void buildIndex(OutputUnitKey key, Map index, Set keys, + OutputUnit root) { + if (index.containsKey(key.getEvaluationPath())) { + return; + } + // Ensure the path is created + JsonNodePath path = key.getEvaluationPath(); + Deque stack = new ArrayDeque<>(); + while (path != null && path.getElement(-1) != null) { + stack.push(path); + path = path.getParent(); + } + + OutputUnit parent = root; + while (!stack.isEmpty()) { + JsonNodePath current = stack.pop(); + if (!index.containsKey(current) && keys.contains(current)) { + // the index doesn't contain this path but this is a path with data + OutputUnit child = new OutputUnit(); + child.setValid(true); + child.setEvaluationPath(current.toString()); + index.put(current, child); + if (parent.getDetails() == null) { + parent.setDetails(new ArrayList<>()); + } + parent.getDetails().add(child); + } + + // If exists in the index this is the new parent + // Otherwise this is an evaluation path with no data and hence should be skipped + OutputUnit child = index.get(current); + if (child != null) { + parent = child; + } + } + } +} diff --git a/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java new file mode 100644 index 000000000..009048588 --- /dev/null +++ b/src/main/java/com/networknt/schema/output/ListOutputUnitFormatter.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 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.output; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.ValidationContext; +import com.networknt.schema.ValidationMessage; + +/** + * ListOutputUnitFormatter. + */ +public class ListOutputUnitFormatter { + public static OutputUnit format(Set validationMessages, ExecutionContext executionContext, + ValidationContext validationContext) { + OutputUnit root = new OutputUnit(); + root.setValid(validationMessages.isEmpty()); + + OutputUnitData data = OutputUnitData.from(validationMessages, executionContext); + + Map valid = data.getValid(); + Map> errors = data.getErrors(); + Map> annotations = data.getAnnotations(); + Map> droppedAnnotations = data.getDroppedAnnotations(); + + // Process the list + for (Entry entry : valid.entrySet()) { + OutputUnit output = new OutputUnit(); + OutputUnitKey key = entry.getKey(); + output.setValid(entry.getValue()); + output.setEvaluationPath(key.getEvaluationPath().toString()); + output.setSchemaLocation(key.getSchemaLocation().toString()); + output.setInstanceLocation(key.getInstanceLocation().toString()); + + // Errors + Map errorMap = errors.get(key); + if (errorMap != null && !errorMap.isEmpty()) { + if (output.getErrors() == null) { + output.setErrors(new LinkedHashMap<>()); + } + for (Entry errorEntry : errorMap.entrySet()) { + output.getErrors().put(errorEntry.getKey(), errorEntry.getValue()); + } + } + + // Annotations + Map annotationsMap = annotations.get(key); + if (annotationsMap != null && !annotationsMap.isEmpty()) { + if (output.getAnnotations() == null) { + output.setAnnotations(new LinkedHashMap<>()); + } + for (Entry annotationEntry : annotationsMap.entrySet()) { + output.getAnnotations().put(annotationEntry.getKey(), annotationEntry.getValue()); + } + } + + // Dropped Annotations + Map droppedAnnotationsMap = droppedAnnotations.get(key); + if (droppedAnnotationsMap != null && !droppedAnnotationsMap.isEmpty()) { + if (output.getDroppedAnnotations() == null) { + output.setDroppedAnnotations(new LinkedHashMap<>()); + } + for (Entry droppedAnnotationEntry : droppedAnnotationsMap.entrySet()) { + output.getDroppedAnnotations().put(droppedAnnotationEntry.getKey(), + droppedAnnotationEntry.getValue()); + } + } + + List details = root.getDetails(); + if (details == null) { + details = new ArrayList<>(); + root.setDetails(details); + } + details.add(output); + } + + return root; + } +} diff --git a/src/main/java/com/networknt/schema/output/OutputFlag.java b/src/main/java/com/networknt/schema/output/OutputFlag.java new file mode 100644 index 000000000..413bce06d --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputFlag.java @@ -0,0 +1,47 @@ +package com.networknt.schema.output; + +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * The Flag output results. + */ +public class OutputFlag { + private final boolean valid; + + public OutputFlag(boolean valid) { + this.valid = valid; + } + + public boolean isValid() { + return this.valid; + } + + @Override + public int hashCode() { + return Objects.hash(valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputFlag other = (OutputFlag) obj; + return valid == other.valid; + } + + @Override + public String toString() { + try { + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + return "OutputFlag [valid=" + valid + "]"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/output/OutputUnit.java b/src/main/java/com/networknt/schema/output/OutputUnit.java new file mode 100644 index 000000000..7f0dac946 --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnit.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2024 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.output; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.networknt.schema.serialization.JsonMapperFactory; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.core.JsonProcessingException; + +/** + * Represents an output unit. + * + * @see A + * Specification for Machine-Readable Output for JSON Schema Validation and + * Annotation + */ +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({ "valid", "evaluationPath", "schemaLocation", "instanceLocation", "errors", "annotations", + "droppedAnnotations", "details" }) +public class OutputUnit { + private boolean valid; + + private String evaluationPath = null; + private String schemaLocation = null; + private String instanceLocation = null; + + private Map errors = null; + + private Map annotations = null; + + private Map droppedAnnotations = null; + + private List details = null; + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + public String getEvaluationPath() { + return evaluationPath; + } + + public void setEvaluationPath(String evaluationPath) { + this.evaluationPath = evaluationPath; + } + + public String getSchemaLocation() { + return schemaLocation; + } + + public void setSchemaLocation(String schemaLocation) { + this.schemaLocation = schemaLocation; + } + + public String getInstanceLocation() { + return instanceLocation; + } + + public void setInstanceLocation(String instanceLocation) { + this.instanceLocation = instanceLocation; + } + + public Map getErrors() { + return errors; + } + + public void setErrors(Map errors) { + this.errors = errors; + } + + public Map getAnnotations() { + return annotations; + } + + public void setAnnotations(Map annotations) { + this.annotations = annotations; + } + + public Map getDroppedAnnotations() { + return droppedAnnotations; + } + + public void setDroppedAnnotations(Map droppedAnnotations) { + this.droppedAnnotations = droppedAnnotations; + } + + public List getDetails() { + return details; + } + + public void setDetails(List details) { + this.details = details; + } + + @Override + public int hashCode() { + return Objects.hash(annotations, details, droppedAnnotations, errors, evaluationPath, instanceLocation, + schemaLocation, valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputUnit other = (OutputUnit) obj; + return Objects.equals(annotations, other.annotations) && Objects.equals(details, other.details) + && Objects.equals(droppedAnnotations, other.droppedAnnotations) && Objects.equals(errors, other.errors) + && Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation) && valid == other.valid; + } + + @Override + public String toString() { + try { + return JsonMapperFactory.getInstance().writerWithDefaultPrettyPrinter().writeValueAsString(this); + } catch (JsonProcessingException e) { + return "OutputUnit [valid=" + valid + ", evaluationPath=" + evaluationPath + ", schemaLocation=" + + schemaLocation + ", instanceLocation=" + instanceLocation + ", errors=" + errors + + ", annotations=" + annotations + ", droppedAnnotations=" + droppedAnnotations + ", details=" + + details + "]"; + } + } +} diff --git a/src/main/java/com/networknt/schema/output/OutputUnitData.java b/src/main/java/com/networknt/schema/output/OutputUnitData.java new file mode 100644 index 000000000..b6ab108ce --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnitData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 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.output; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.networknt.schema.ExecutionContext; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.ValidationMessage; +import com.networknt.schema.annotation.JsonNodeAnnotation; + +/** + * Output Unit Data. + */ +public class OutputUnitData { + private final Map valid = new LinkedHashMap<>(); + private final Map> errors = new LinkedHashMap<>(); + private final Map> annotations = new LinkedHashMap<>(); + private final Map> droppedAnnotations = new LinkedHashMap<>(); + + public Map getValid() { + return valid; + } + + public Map> getErrors() { + return errors; + } + + public Map> getAnnotations() { + return annotations; + } + + public Map> getDroppedAnnotations() { + return droppedAnnotations; + } + + public static String formatMessage(String message) { + int index = message.indexOf(":"); + if (index != -1) { + int length = message.length(); + while (index + 1 < length) { + if (message.charAt(index + 1) == ' ') { + index++; + } else { + break; + } + } + return message.substring(index + 1); + } + return message; + } + + public static OutputUnitData from(Set validationMessages, ExecutionContext executionContext) { + OutputUnitData data = new OutputUnitData(); + + Map valid = data.valid; + Map> errors = data.errors; + Map> annotations = data.annotations; + Map> droppedAnnotations = data.droppedAnnotations; + + for (ValidationMessage assertion : validationMessages) { + SchemaLocation assertionSchemaLocation = new SchemaLocation(assertion.getSchemaLocation().getAbsoluteIri(), + assertion.getSchemaLocation().getFragment().getParent()); + OutputUnitKey key = new OutputUnitKey(assertion.getEvaluationPath().getParent(), + assertionSchemaLocation, assertion.getInstanceLocation()); + valid.put(key, false); + Map errorMap = errors.computeIfAbsent(key, k -> new LinkedHashMap<>()); + errorMap.put(assertion.getType(), formatMessage(assertion.getMessage())); + } + + for (List annotationsResult : executionContext.getAnnotations().asMap().values()) { + for (JsonNodeAnnotation annotation : annotationsResult) { + // As some annotations are required for computation, filter those that are not + // required for reporting + if (executionContext.getExecutionConfig().getAnnotationCollectionPredicate() + .test(annotation.getKeyword())) { + SchemaLocation annotationSchemaLocation = new SchemaLocation( + annotation.getSchemaLocation().getAbsoluteIri(), + annotation.getSchemaLocation().getFragment().getParent()); + + OutputUnitKey key = new OutputUnitKey(annotation.getEvaluationPath().getParent(), + annotationSchemaLocation, annotation.getInstanceLocation()); + boolean validResult = executionContext.getResults().isValid(annotation.getInstanceLocation(), + annotation.getEvaluationPath()); + valid.put(key, validResult); + if (validResult) { + // annotations + Map annotationMap = annotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + annotationMap.put(annotation.getKeyword(), annotation.getValue()); + } else { + // dropped annotations + Map droppedAnnotationMap = droppedAnnotations.computeIfAbsent(key, + k -> new LinkedHashMap<>()); + droppedAnnotationMap.put(annotation.getKeyword(), annotation.getValue()); + } + } + } + } + return data; + } +} diff --git a/src/main/java/com/networknt/schema/output/OutputUnitKey.java b/src/main/java/com/networknt/schema/output/OutputUnitKey.java new file mode 100644 index 000000000..5c99151af --- /dev/null +++ b/src/main/java/com/networknt/schema/output/OutputUnitKey.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 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.output; + +import java.util.Objects; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * Output Unit Key. + */ +public class OutputUnitKey { + final JsonNodePath evaluationPath; + final SchemaLocation schemaLocation; + final JsonNodePath instanceLocation; + + public OutputUnitKey(JsonNodePath evaluationPath, SchemaLocation schemaLocation, JsonNodePath instanceLocation) { + super(); + this.evaluationPath = evaluationPath; + this.schemaLocation = schemaLocation; + this.instanceLocation = instanceLocation; + } + + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + public JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, schemaLocation); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + OutputUnitKey other = (OutputUnitKey) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation); + } +} \ No newline at end of file diff --git a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java index 86b4633ac..6d4b32f53 100644 --- a/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaLoader.java @@ -12,15 +12,47 @@ */ public class MapSchemaLoader implements SchemaLoader { private final Function mappings; - + + /** + * Sets the schema data by absolute IRI. + * + * @param mappings the mappings + */ public MapSchemaLoader(Map mappings) { this(mappings::get); } - + + /** + * Sets the schema data by absolute IRI function. + * + * @param mappings the mappings + */ public MapSchemaLoader(Function mappings) { this.mappings = mappings; } - + + /** + * Sets the schema data by using two mapping functions. + *

+ * Firstly to map the IRI to an object. If the object is null no mapping is + * performed. + *

+ * Next to map the object to the schema data. + * + * @param the type of the object + * @param mapIriToObject the mapping of IRI to object + * @param mapObjectToData the mappingof object to schema data + */ + public MapSchemaLoader(Function mapIriToObject, Function mapObjectToData) { + this.mappings = iri -> { + T result = mapIriToObject.apply(iri); + if (result != null) { + return mapObjectToData.apply(result); + } + return null; + }; + } + @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { try { diff --git a/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java index 1e7b34f2b..a5e8825c9 100644 --- a/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java +++ b/src/main/java/com/networknt/schema/resource/MapSchemaMapper.java @@ -2,6 +2,7 @@ import java.util.Map; import java.util.function.Function; +import java.util.function.Predicate; import com.networknt.schema.AbsoluteIri; @@ -14,11 +15,26 @@ public class MapSchemaMapper implements SchemaMapper { public MapSchemaMapper(Map mappings) { this(mappings::get); } - + public MapSchemaMapper(Function mappings) { this.mappings = mappings; } - + + /** + * Apply the mapping function if the predicate is true. + * + * @param test the predicate + * @param mappings the mapping + */ + public MapSchemaMapper(Predicate test, Function mappings) { + this.mappings = iri -> { + if (test.test(iri)) { + return mappings.apply(iri); + } + return null; + }; + } + @Override public AbsoluteIri map(AbsoluteIri absoluteIRI) { String mapped = this.mappings.apply(absoluteIRI.toString()); diff --git a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java index 607c4250c..dbe847c6d 100644 --- a/src/main/java/com/networknt/schema/resource/SchemaLoaders.java +++ b/src/main/java/com/networknt/schema/resource/SchemaLoaders.java @@ -105,6 +105,24 @@ public Builder schemas(Function schemas) { return this; } + /** + * Sets the schema data by using two mapping functions. + *

+ * Firstly to map the IRI to an object. If the object is null no mapping is + * performed. + *

+ * Next to map the object to the schema data. + * + * @param the type of the object + * @param mapIriToObject the mapping of IRI to object + * @param mapObjectToData the mappingof object to schema data + * @return the builder + */ + public Builder schemas(Function mapIriToObject, Function mapObjectToData) { + this.values.add(new MapSchemaLoader(mapIriToObject, mapObjectToData)); + return this; + } + /** * Builds a {@link SchemaLoaders}. * diff --git a/src/main/java/com/networknt/schema/resource/SchemaMappers.java b/src/main/java/com/networknt/schema/resource/SchemaMappers.java index 2b0905943..1bdc79e2d 100644 --- a/src/main/java/com/networknt/schema/resource/SchemaMappers.java +++ b/src/main/java/com/networknt/schema/resource/SchemaMappers.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; /** * Schema Mappers used to map an ID indicated by an absolute IRI to a retrieval @@ -118,6 +119,18 @@ public Builder mappings(Function mappings) { return this; } + /** + * Sets the function that maps the IRI to another IRI if the predicate is true. + * + * @param test the predicate + * @param mappings the mappings + * @return the builder + */ + public Builder mappings(Predicate test, Function mappings) { + this.values.add(new MapSchemaMapper(test, mappings)); + return this; + } + /** * Builds a {@link SchemaMappers} * diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResult.java b/src/main/java/com/networknt/schema/result/JsonNodeResult.java new file mode 100644 index 000000000..8b464d37b --- /dev/null +++ b/src/main/java/com/networknt/schema/result/JsonNodeResult.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2023 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.result; + +import java.util.Objects; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * Sub schema results. + */ +public class JsonNodeResult { + private final JsonNodePath instanceLocation; + private final SchemaLocation schemaLocation; + private final JsonNodePath evaluationPath; + private final boolean valid; + + public JsonNodeResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, + boolean valid) { + super(); + this.instanceLocation = instanceLocation; + this.schemaLocation = schemaLocation; + this.evaluationPath = evaluationPath; + this.valid = valid; + } + + public JsonNodePath getInstanceLocation() { + return instanceLocation; + } + + public SchemaLocation getSchemaLocation() { + return schemaLocation; + } + + public JsonNodePath getEvaluationPath() { + return evaluationPath; + } + + public boolean isValid() { + return valid; + } + + @Override + public String toString() { + return "JsonNodeResult [instanceLocation=" + instanceLocation + ", schemaLocation=" + schemaLocation + + ", evaluationPath=" + evaluationPath + ", valid=" + valid + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(evaluationPath, instanceLocation, schemaLocation, valid); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JsonNodeResult other = (JsonNodeResult) obj; + return Objects.equals(evaluationPath, other.evaluationPath) + && Objects.equals(instanceLocation, other.instanceLocation) + && Objects.equals(schemaLocation, other.schemaLocation) && valid == other.valid; + } + +} diff --git a/src/main/java/com/networknt/schema/result/JsonNodeResults.java b/src/main/java/com/networknt/schema/result/JsonNodeResults.java new file mode 100644 index 000000000..07a419441 --- /dev/null +++ b/src/main/java/com/networknt/schema/result/JsonNodeResults.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 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.result; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.networknt.schema.JsonNodePath; +import com.networknt.schema.SchemaLocation; + +/** + * Sub schema results. + */ +public class JsonNodeResults { + + /** + * Stores the invalid results. + */ + private Map> values = new HashMap<>(); + + public void setResult(JsonNodePath instanceLocation, SchemaLocation schemaLocation, JsonNodePath evaluationPath, + boolean valid) { + JsonNodeResult result = new JsonNodeResult(instanceLocation, schemaLocation, evaluationPath, valid); + List v = values.computeIfAbsent(instanceLocation, k -> new ArrayList<>()); + v.add(result); + } + + public boolean isValid(JsonNodePath instanceLocation, JsonNodePath evaluationPath) { + List instance = values.get(instanceLocation); + if (instance != null) { + for (JsonNodeResult result : instance) { + if (evaluationPath.startsWith(result.getEvaluationPath().getParent())) { + if(!result.isValid()) { + return false; + } + } + } + } + return true; + } + +} diff --git a/src/main/resources/jsv-messages.properties b/src/main/resources/jsv-messages.properties index 374547c81..52819b4f4 100644 --- a/src/main/resources/jsv-messages.properties +++ b/src/main/resources/jsv-messages.properties @@ -42,7 +42,7 @@ propertyNames = Property name {0} is not valid for validation: {1} readOnly = {0}: is a readonly field, it cannot be changed required = {0}: required property ''{1}'' not found type = {0}: {1} found, {2} expected -unevaluatedItems = {0}: must not have unevaluated items +unevaluatedItems = {0}: must not have unevaluated items or must match unevaluated items schema unevaluatedProperties = {0}: must not have unevaluated properties unionType = {0}: {1} found, but {2} is required uniqueItems = {0}: the items in the array must be unique diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java index 779c27273..a7956b9b8 100644 --- a/src/test/java/com/networknt/schema/CollectorContextTest.java +++ b/src/test/java/com/networknt/schema/CollectorContextTest.java @@ -255,7 +255,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(schemaLocation, evaluationPath); + return new CustomValidator(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -268,8 +268,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev * document again just for gathering this kind of information. */ private class CustomValidator extends AbstractJsonValidator { - public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new CustomKeyword(), schemaNode); } @Override @@ -326,7 +326,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException, Exception { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator1(schemaLocation, evaluationPath); + return new CustomValidator1(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -341,8 +341,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev * keyword has been used multiple times in JSON Schema. */ private class CustomValidator1 extends AbstractJsonValidator { - public CustomValidator1(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator1(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath,new CustomKeyword(), schemaNode); } @SuppressWarnings("unchecked") diff --git a/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java new file mode 100644 index 000000000..b0d16c68b --- /dev/null +++ b/src/test/java/com/networknt/schema/ContentSchemaValidatorTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.output.OutputUnit; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * ContentSchemaValidatorTest. + */ +public class ContentSchemaValidatorTest { + @Test + void annotationCollection() throws JsonProcessingException { + String schemaData = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"contentMediaType\": \"application/jwt\",\r\n" + + " \"contentSchema\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 2,\r\n" + + " \"prefixItems\": [\r\n" + + " {\r\n" + + " \"const\": {\r\n" + + " \"typ\": \"JWT\",\r\n" + + " \"alg\": \"HS256\"\r\n" + + " }\r\n" + + " },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\"iss\", \"exp\"],\r\n" + + " \"properties\": {\r\n" + + " \"iss\": {\"type\": \"string\"},\r\n" + + " \"exp\": {\"type\": \"integer\"}\r\n" + + " }\r\n" + + " }\r\n" + + " ]\r\n" + + " }\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = "\"helloworld\""; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":true,\"details\":[{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"#\",\"instanceLocation\":\"\",\"annotations\":{\"contentMediaType\":\"application/jwt\",\"contentSchema\":{\"type\":\"array\",\"minItems\":2,\"prefixItems\":[{\"const\":{\"typ\":\"JWT\",\"alg\":\"HS256\"}},{\"type\":\"object\",\"required\":[\"iss\",\"exp\"],\"properties\":{\"iss\":{\"type\":\"string\"},\"exp\":{\"type\":\"integer\"}}}]}}}]}"; + assertEquals(expected, output); + } +} diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java index 7e427c2ef..63d84a1e1 100644 --- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java +++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java @@ -49,8 +49,8 @@ private static final class Validator extends AbstractJsonValidator { private final String keyword; private Validator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, String keyword, - List enumValues, List enumNames) { - super(schemaLocation, evaluationPath,null); + List enumValues, List enumNames, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new EnumNamesKeyword(), schemaNode); if (enumNames.size() != enumValues.size()) { throw new IllegalArgumentException("enum and enumNames need to be of same length"); } @@ -69,6 +69,8 @@ public Set validate(ExecutionContext executionContext, JsonNo String valueName = enumNames.get(idx); Set messages = new HashSet<>(); ValidationMessage validationMessage = ValidationMessage.builder().type(keyword) + .schemaNode(node) + .instanceNode(node) .code("tests.example.enumNames").message("{0}: enumName is {1}").instanceLocation(instanceLocation) .arguments(valueName).build(); messages.add(validationMessage); @@ -96,7 +98,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev } JsonNode enumSchemaNode = parentSchemaNode.get("enum"); - return new Validator(schemaLocation, evaluationPath, getValue(), readStringList(enumSchemaNode), readStringList(schemaNode)); + return new Validator(schemaLocation, evaluationPath, getValue(), readStringList(enumSchemaNode), + readStringList(schemaNode), schemaNode); } private List readStringList(JsonNode node) { diff --git a/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java b/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java new file mode 100644 index 000000000..cac5ef937 --- /dev/null +++ b/src/test/java/com/networknt/schema/DefaultJsonSchemaIdValidatorTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +/** + * Tests for the non-standard DefaultJsonSchemaIdValidator. + */ +public class DefaultJsonSchemaIdValidatorTest { + @Test + void givenRelativeIdShouldThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + try { + JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config); + } catch (InvalidSchemaException e) { + assertEquals("/$id: # is an invalid segment for URI 0", e.getMessage()); + } + } + + @Test + void givenFragmentWithNoContextShouldNotThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"#0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + } + + @Test + void givenSlashWithNoContextShouldNotThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"/base\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + } + + @Test + void givenRelativeIdWithClasspathBaseShouldNotThrowInvalidSchemaException() { + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertDoesNotThrow(() -> JsonSchemaFactory.getInstance(VersionFlag.V202012) + .getSchema(SchemaLocation.of("classpath:schema/id-relative.json"), config)); + } +} diff --git a/src/test/java/com/networknt/schema/Issue347Test.java b/src/test/java/com/networknt/schema/Issue347Test.java index 331ba9fb3..d8301e20f 100644 --- a/src/test/java/com/networknt/schema/Issue347Test.java +++ b/src/test/java/com/networknt/schema/Issue347Test.java @@ -3,6 +3,7 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import org.junit.jupiter.api.Test; @@ -11,11 +12,12 @@ public class Issue347Test { @Test public void failure() { JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); + assertThrows(JsonSchemaException.class, () -> factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json"))); try { - JsonSchema schema = factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json")); + factory.getSchema(Thread.currentThread().getContextClassLoader().getResourceAsStream("schema/issue347-v7.json")); } catch (Throwable e) { assertThat(e, instanceOf(JsonSchemaException.class)); - assertEquals("/$id: null is an invalid segment for URI test", e.getMessage()); + assertEquals("/$id: # is an invalid segment for URI test", e.getMessage()); } } } diff --git a/src/test/java/com/networknt/schema/Issue366FailFastTest.java b/src/test/java/com/networknt/schema/Issue366FailFastTest.java index eab951fc6..55c94abbf 100644 --- a/src/test/java/com/networknt/schema/Issue366FailFastTest.java +++ b/src/test/java/com/networknt/schema/Issue366FailFastTest.java @@ -1,7 +1,7 @@ package com.networknt.schema; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -14,92 +14,87 @@ public class Issue366FailFastTest { - @BeforeEach - public void setup() throws IOException { - setupSchema(); - } - - JsonSchema jsonSchema; - ObjectMapper objectMapper = new ObjectMapper(); - private void setupSchema() throws IOException { - - SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); - schemaValidatorsConfig.setFailFast(true); - JsonSchemaFactory schemaFactory = JsonSchemaFactory - .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)) - .jsonMapper(objectMapper) - .build(); - - schemaValidatorsConfig.setTypeLoose(false); - - SchemaLocation uri = getSchema(); - - InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); - JsonNode testCases = objectMapper.readValue(in, JsonNode.class); - this.jsonSchema = schemaFactory.getSchema(uri, testCases,schemaValidatorsConfig); - } - - protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception { - ObjectMapper mapper = new ObjectMapper(); - JsonNode node = mapper.readTree(content); - return node; - } - - @Test - public void firstOneValid() throws Exception { - String dataPath = "/data/issue366.json"; - - InputStream dataInputStream = getClass().getResourceAsStream(dataPath); - JsonNode node = getJsonNodeFromStreamContent(dataInputStream); - List testNodes = node.findValues("tests"); - JsonNode testNode = testNodes.get(0).get(0); - JsonNode dataNode = testNode.get("data"); - Set errors = jsonSchema.validate(dataNode); - assertTrue(errors.isEmpty()); - } - - @Test - public void secondOneValid() throws Exception { - String dataPath = "/data/issue366.json"; - - InputStream dataInputStream = getClass().getResourceAsStream(dataPath); - JsonNode node = getJsonNodeFromStreamContent(dataInputStream); - List testNodes = node.findValues("tests"); - JsonNode testNode = testNodes.get(0).get(1); - JsonNode dataNode = testNode.get("data"); - Set errors = jsonSchema.validate(dataNode); - assertTrue(errors.isEmpty()); - } - - @Test - public void bothValid() throws Exception { - String dataPath = "/data/issue366.json"; - - assertThrows(JsonSchemaException.class, () -> { + @BeforeEach + public void setup() throws IOException { + setupSchema(); + } + + JsonSchema jsonSchema; + ObjectMapper objectMapper = new ObjectMapper(); + + private void setupSchema() throws IOException { + + SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig(); + schemaValidatorsConfig.setFailFast(true); + JsonSchemaFactory schemaFactory = JsonSchemaFactory + .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7)).jsonMapper(objectMapper).build(); + + schemaValidatorsConfig.setTypeLoose(false); + + SchemaLocation uri = getSchema(); + + InputStream in = getClass().getResourceAsStream("/schema/issue366_schema.json"); + JsonNode testCases = objectMapper.readValue(in, JsonNode.class); + this.jsonSchema = schemaFactory.getSchema(uri, testCases, schemaValidatorsConfig); + } + + protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(content); + return node; + } + + @Test + public void firstOneValid() throws Exception { + String dataPath = "/data/issue366.json"; + + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); + JsonNode node = getJsonNodeFromStreamContent(dataInputStream); + List testNodes = node.findValues("tests"); + JsonNode testNode = testNodes.get(0).get(0); + JsonNode dataNode = testNode.get("data"); + Set errors = jsonSchema.validate(dataNode); + assertTrue(errors.isEmpty()); + } + + @Test + public void secondOneValid() throws Exception { + String dataPath = "/data/issue366.json"; + + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); + JsonNode node = getJsonNodeFromStreamContent(dataInputStream); + List testNodes = node.findValues("tests"); + JsonNode testNode = testNodes.get(0).get(1); + JsonNode dataNode = testNode.get("data"); + Set errors = jsonSchema.validate(dataNode); + assertTrue(errors.isEmpty()); + } + + @Test + public void bothValid() throws Exception { + String dataPath = "/data/issue366.json"; + InputStream dataInputStream = getClass().getResourceAsStream(dataPath); JsonNode node = getJsonNodeFromStreamContent(dataInputStream); List testNodes = node.findValues("tests"); JsonNode testNode = testNodes.get(0).get(2); JsonNode dataNode = testNode.get("data"); - jsonSchema.validate(dataNode); - }); - } + assertEquals(1, jsonSchema.validate(dataNode).size()); + } - @Test - public void neitherValid() throws Exception { - String dataPath = "/data/issue366.json"; + @Test + public void neitherValid() throws Exception { + String dataPath = "/data/issue366.json"; - assertThrows(JsonSchemaException.class, () -> { InputStream dataInputStream = getClass().getResourceAsStream(dataPath); JsonNode node = getJsonNodeFromStreamContent(dataInputStream); List testNodes = node.findValues("tests"); JsonNode testNode = testNodes.get(0).get(3); JsonNode dataNode = testNode.get("data"); - jsonSchema.validate(dataNode); - }); - } + assertEquals(1, jsonSchema.validate(dataNode).size()); + } - private SchemaLocation getSchema() { - return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); - } + private SchemaLocation getSchema() { + return SchemaLocation.of("classpath:" + "/draft7/issue366_schema.json"); + } } diff --git a/src/test/java/com/networknt/schema/Issue456Test.java b/src/test/java/com/networknt/schema/Issue456Test.java index c96c389d0..723637fd8 100644 --- a/src/test/java/com/networknt/schema/Issue456Test.java +++ b/src/test/java/com/networknt/schema/Issue456Test.java @@ -24,7 +24,7 @@ protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exce public void shouldWorkT2() throws Exception { String schemaPath = "/schema/issue456-v7.json"; String dataPath = "/data/issue456-T2.json"; - String dataT3Path = "/data/issue456-T3.json"; +// String dataT3Path = "/data/issue456-T3.json"; InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath); JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream); InputStream dataInputStream = getClass().getResourceAsStream(dataPath); diff --git a/src/test/java/com/networknt/schema/Issue792.java b/src/test/java/com/networknt/schema/Issue792.java index 2ca3f238e..935336e6b 100644 --- a/src/test/java/com/networknt/schema/Issue792.java +++ b/src/test/java/com/networknt/schema/Issue792.java @@ -3,7 +3,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import static org.junit.jupiter.api.Assertions.assertThrows; + +import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ void test() throws JsonProcessingException { JsonSchema jsonSchema = schemaFactory.getSchema(schemaDef, config); JsonNode jsonNode = new ObjectMapper().readTree("{\"field\": \"pattern-violation\"}"); - //this works with 1.0.81, but not with 1.0.82+ - assertThrows(JsonSchemaException.class, () -> jsonSchema.validate(jsonNode)); + assertEquals(1, jsonSchema.validate(jsonNode).size()); } } diff --git a/src/test/java/com/networknt/schema/Issue857Test.java b/src/test/java/com/networknt/schema/Issue857Test.java new file mode 100644 index 000000000..8d533e5ea --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue857Test.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue857Test { + @Test + void test() { + String schema = "{\r\n" + + " \"type\": \"object\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"properties\": {\r\n" + + " \"id\": {\r\n" + + " \"not\": {\r\n" + + " \"enum\": [\r\n" + + " \"1\",\r\n" + + " \"2\",\r\n" + + " \"3\"\r\n" + + " ]\r\n" + + " },\r\n" + + " \"type\": \"string\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"$id\": \"https://d73abc/filter.json\"\r\n" + + "}"; + + String input = "{\r\n" + + " \"id\": \"4\"\r\n" + + "}"; + + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setFailFast(true); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + Set result = factory.getSchema(schema, config).validate(input, InputFormat.JSON); + assertTrue(result.isEmpty()); + } +} diff --git a/src/test/java/com/networknt/schema/Issue927Test.java b/src/test/java/com/networknt/schema/Issue927Test.java new file mode 100644 index 000000000..5a56e2c8d --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue927Test.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * Test that the code isn't confused by an anchor in the id. + */ +public class Issue927Test { + @Test + void test() throws JsonMappingException, JsonProcessingException { + String schema = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"$id\": \"id\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"anyOf\": [\r\n" + + " {\r\n" + + " \"required\": [\r\n" + + " \"id\",\r\n" + + " \"type\",\r\n" + + " \"genericSubmission\"\r\n" + + " ]\r\n" + + " }\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"id\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\"\r\n" + + " },\r\n" + + " \"type\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\"\r\n" + + " },\r\n" + + " \"genericSubmission\": {\r\n" + + " \"$ref\": \"#/definitions/genericSubmission\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"definitions\": {\r\n" + + " \"genericSubmission\": {\r\n" + + " \"$id\": \"#/definitions/genericSubmission\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"required\": [\r\n" + + " \"transactionReference\",\r\n" + + " \"title\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"transactionReference\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"title\": \"title\",\r\n" + + " \"description\": \"description\"\r\n" + + " },\r\n" + + " \"title\": {\r\n" + + " \"type\": \"array\",\r\n" + + " \"minItems\": 1,\r\n" + + " \"items\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"required\": [\r\n" + + " \"value\",\r\n" + + " \"locale\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"value\": {\r\n" + + " \"$ref\": \"#/definitions/value\"\r\n" + + " },\r\n" + + " \"locale\": {\r\n" + + " \"$ref\": \"#/definitions/locale\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " },\r\n" + + " \"value\": {\r\n" + + " \"$id\": \"#/definitions/value\",\r\n" + + " \"type\": \"string\"\r\n" + + " },\r\n" + + " \"locale\": {\r\n" + + " \"$id\": \"#/definitions/locale\",\r\n" + + " \"type\": \"string\",\r\n" + + " \"default\": \"fr\"\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7) + .getSchema(SchemaLocation.of("http://www.example.org"), JsonMapperFactory.getInstance().readTree(schema)); + + String input = "{\r\n" + + " \"$schema\": \"./mySchema.json\",\r\n" + + " \"_comment\": \"comment\",\r\n" + + " \"id\": \"b34024c4-6103-478c-bad6-83b26d98a892\",\r\n" + + " \"type\": \"genericSubmission\",\r\n" + + " \"genericSubmission\": {\r\n" + + " \"transactionReference\": \"123456\",\r\n" + + " \"title\": [\r\n" + + " {\r\n" + + " \"value\": \"[DE]...\",\r\n" + + " \"locale\": \"de\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"value\": \"[EN]...\",\r\n" + + " \"locale\": \"en\"\r\n" + + " }\r\n" + + " ]\r\n" + + " }\r\n" + + "}"; + Set messages = jsonSchema.validate(input, InputFormat.JSON); + assertEquals(0, messages.size()); + } + +} diff --git a/src/test/java/com/networknt/schema/Issue935Test.java b/src/test/java/com/networknt/schema/Issue935Test.java new file mode 100644 index 000000000..fdc018609 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue935Test.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue935Test { + @Test + void shouldThrowInvalidSchemaException() { + String schema = "{ \"$schema\": \"0\" }"; + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V201909).getSchema(schema)); + } +} diff --git a/src/test/java/com/networknt/schema/Issue936Test.java b/src/test/java/com/networknt/schema/Issue936Test.java new file mode 100644 index 000000000..f5c72cc11 --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue936Test.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue936Test { + @Test + void shouldThrowInvalidSchemaException() { + String schema = "{\r\n" + " \"$id\": \"0\",\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\r\n" + "}"; + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setSchemaIdValidator(JsonSchemaIdValidator.DEFAULT); + assertThrowsExactly(InvalidSchemaException.class, + () -> JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config)); + try { + JsonSchemaFactory.getInstance(VersionFlag.V202012).getSchema(schema, config); + } catch (InvalidSchemaException e) { + assertEquals("/$id: # is an invalid segment for URI 0", e.getMessage()); + } + } +} diff --git a/src/test/java/com/networknt/schema/Issue939Test.java b/src/test/java/com/networknt/schema/Issue939Test.java new file mode 100644 index 000000000..19b36c73d --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue939Test.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue939Test { + @Test + void shouldNotThrowException() { + String schema = "{\r\n" + + " \"$schema\": \"http://json-schema.org/draft-07/schema#\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"additionalProperties\": false,\r\n" + + " \"required\": [\r\n" + + " \"someUuid\"\r\n" + + " ],\r\n" + + " \"properties\": {\r\n" + + " \"someUuid\": {\r\n" + + " \"$ref\": \"#/definitions/uuid\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"definitions\": {\r\n" + + " \"uuid\": {\r\n" + + " \"type\": \"string\",\r\n" + + " \"pattern\": \"^[0-9a-f]{8}(\\\\\\\\-[0-9a-f]{4}){3}\\\\\\\\-[0-9a-f]{12}$\",\r\n" + + " \"minLength\": 36,\r\n" + + " \"maxLength\": 36\r\n" + + " }\r\n" + + " }\r\n" + + " }"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schema); + assertDoesNotThrow(() -> jsonSchema.initializeValidators()); + Set assertions = jsonSchema + .validate("{\"someUuid\":\"invalid\"}", InputFormat.JSON); + assertEquals(2, assertions.size()); + } +} diff --git a/src/test/java/com/networknt/schema/Issue940Test.java b/src/test/java/com/networknt/schema/Issue940Test.java new file mode 100644 index 000000000..de30e9bff --- /dev/null +++ b/src/test/java/com/networknt/schema/Issue940Test.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.SpecVersion.VersionFlag; + +public class Issue940Test { + @Test + void shouldNotThrowException() { + String schema = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$ref\": \"#/$defs/greeting\",\r\n" + + " \"$defs\": {\r\n" + + " \"greeting\": {}\r\n" + + " }\r\n" + + "}"; + JsonSchema jsonSchema = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(schema); + assertDoesNotThrow(() -> jsonSchema.initializeValidators()); + } +} diff --git a/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java new file mode 100644 index 000000000..596393985 --- /dev/null +++ b/src/test/java/com/networknt/schema/JsonNodeAnnotationsTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.annotation.JsonNodeAnnotation; +import com.networknt.schema.annotation.JsonNodeAnnotations; + +/** + * JsonNodeAnnotationsTest. + */ +class JsonNodeAnnotationsTest { + @Test + void put() { + JsonNodeAnnotations annotations = new JsonNodeAnnotations(); + JsonNodeAnnotation annotation = new JsonNodeAnnotation("unevaluatedProperties", + new JsonNodePath(PathType.JSON_POINTER), SchemaLocation.of(""), new JsonNodePath(PathType.JSON_POINTER), + "test"); + annotations.put(annotation); + assertTrue(annotations.asMap().get(annotation.getInstanceLocation()).contains(annotation)); + } +} diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java index 50a14608b..788dcd79a 100644 --- a/src/test/java/com/networknt/schema/JsonWalkTest.java +++ b/src/test/java/com/networknt/schema/JsonWalkTest.java @@ -127,7 +127,7 @@ public String getValue() { public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) throws JsonSchemaException { if (schemaNode != null && schemaNode.isArray()) { - return new CustomValidator(schemaLocation, evaluationPath); + return new CustomValidator(schemaLocation, evaluationPath, schemaNode); } return null; } @@ -140,8 +140,8 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev */ private static class CustomValidator extends AbstractJsonValidator { - public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath) { - super(schemaLocation, evaluationPath,null); + public CustomValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode) { + super(schemaLocation, evaluationPath, new CustomKeyword(), schemaNode); } @Override diff --git a/src/test/java/com/networknt/schema/OutputUnitTest.java b/src/test/java/com/networknt/schema/OutputUnitTest.java new file mode 100644 index 000000000..cfa0bdf4d --- /dev/null +++ b/src/test/java/com/networknt/schema/OutputUnitTest.java @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2024 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; + +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 static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.output.OutputUnit; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * OutputUnitTest. + * + * @see A + * Specification for Machine-Readable Output for JSON Schema Validation and + * Annotation + */ +public class OutputUnitTest { + String schemaData = "{\r\n" + + " \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\r\n" + + " \"$id\": \"https://json-schema.org/schemas/example\",\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"root\",\r\n" + + " \"properties\": {\r\n" + + " \"foo\": {\r\n" + + " \"allOf\": [\r\n" + + " { \"required\": [\"unspecified-prop\"] },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"foo-title\",\r\n" + + " \"properties\": {\r\n" + + " \"foo-prop\": {\r\n" + + " \"const\": 1,\r\n" + + " \"title\": \"foo-prop-title\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"additionalProperties\": { \"type\": \"boolean\" }\r\n" + + " }\r\n" + + " ]\r\n" + + " },\r\n" + + " \"bar\": { \"$ref\": \"#/$defs/bar\" }\r\n" + + " },\r\n" + + " \"$defs\": {\r\n" + + " \"bar\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"title\": \"bar-title\",\r\n" + + " \"properties\": {\r\n" + + " \"bar-prop\": {\r\n" + + " \"type\": \"integer\",\r\n" + + " \"minimum\": 10,\r\n" + + " \"title\": \"bar-prop-title\"\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + String inputData1 = "{\r\n" + + " \"foo\": { \"foo-prop\": \"not 1\", \"other-prop\": false },\r\n" + + " \"bar\": { \"bar-prop\": 2 }\r\n" + + "}"; + + String inputData2 = "{\r\n" + + " \"foo\": {\r\n" + + " \"foo-prop\": 1,\r\n" + + " \"unspecified-prop\": true\r\n" + + " },\r\n" + + " \"bar\": { \"bar-prop\": 20 }\r\n" + + "}"; + @Test + void annotationCollectionList() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData1; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":false,\"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/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/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\"}},{\"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\"]}},{\"valid\":false,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"droppedAnnotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"}},{\"valid\":false,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"droppedAnnotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"}}]}"; + assertEquals(expected, output); + } + + @Test + void annotationCollectionHierarchical() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData1; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"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\"}}]}]}"; + assertEquals(expected, output); + } + + @Test + void annotationCollectionHierarchical2() throws JsonProcessingException { + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(schemaData, config); + + String inputData = inputData2; + + OutputUnit outputUnit = schema.validate(inputData, InputFormat.JSON, OutputFormat.HIERARCHICAL, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + String output = JsonMapperFactory.getInstance().writeValueAsString(outputUnit); + String expected = "{\"valid\":true,\"evaluationPath\":\"\",\"schemaLocation\":\"https://json-schema.org/schemas/example#\",\"instanceLocation\":\"\",\"annotations\":{\"properties\":[\"foo\",\"bar\"],\"title\":\"root\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/foo/allOf/1\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/properties/foo/allOf/1\",\"instanceLocation\":\"/foo\",\"annotations\":{\"properties\":[\"foo-prop\"],\"title\":\"foo-title\",\"additionalProperties\":[\"foo-prop\",\"unspecified-prop\"]},\"details\":[{\"valid\":true,\"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\",\"annotations\":{\"title\":\"foo-prop-title\"}}]},{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar\",\"instanceLocation\":\"/bar\",\"annotations\":{\"properties\":[\"bar-prop\"],\"title\":\"bar-title\"},\"details\":[{\"valid\":true,\"evaluationPath\":\"/properties/bar/$ref/properties/bar-prop\",\"schemaLocation\":\"https://json-schema.org/schemas/example#/$defs/bar/properties/bar-prop\",\"instanceLocation\":\"/bar/bar-prop\",\"annotations\":{\"title\":\"bar-prop-title\"}}]}]}"; + assertEquals(expected, output); + } + + enum FormatInput { + DATE_TIME("date-time"), + DATE("date"), + TIME("time"), + DURATION("duration"), + EMAIL("email"), + IDN_EMAIL("idn-email"), + HOSTNAME("hostname"), + IDN_HOSTNAME("idn-hostname"), + IPV4("ipv4"), + IPV6("ipv6"), + URI("uri"), + URI_REFERENCE("uri-reference"), + IRI("iri"), + IRI_REFERENCE("iri-reference"), + UUID("uuid"), + JSON_POINTER("json-pointer"), + RELATIVE_JSON_POINTER("relative-json-pointer"), + REGEX("regex"); + + String format; + + FormatInput(String format) { + this.format = format; + } + } + + @ParameterizedTest + @EnumSource(FormatInput.class) + void formatAnnotation(FormatInput formatInput) { + String formatSchema = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"format\": \""+formatInput.format+"\"\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(formatSchema, config); + OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + assertTrue(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertEquals(formatInput.format, details.getAnnotations().get("format")); + } + + @ParameterizedTest + @EnumSource(FormatInput.class) + void formatAssertion(FormatInput formatInput) { + String formatSchema = "{\r\n" + + " \"type\": \"string\",\r\n" + + " \"format\": \""+formatInput.format+"\"\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(formatSchema, config); + OutputUnit outputUnit = schema.validate("\"inval!i:d^(abc]\"", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + executionConfiguration.getExecutionConfig().setFormatAssertionsEnabled(true); + }); + assertFalse(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertEquals(formatInput.format, details.getDroppedAnnotations().get("format")); + assertNotNull(details.getErrors().get("format")); + } + + @Test + void typeUnion() { + String typeSchema = "{\r\n" + + " \"type\": [\"string\",\"array\"]\r\n" + + "}"; + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setPathType(PathType.JSON_POINTER); + JsonSchema schema = factory.getSchema(typeSchema, config); + OutputUnit outputUnit = schema.validate("1", InputFormat.JSON, OutputFormat.LIST, executionConfiguration -> { + executionConfiguration.getExecutionConfig().setAnnotationCollectionEnabled(true); + executionConfiguration.getExecutionConfig().setAnnotationCollectionPredicate(keyword -> true); + }); + assertFalse(outputUnit.isValid()); + OutputUnit details = outputUnit.getDetails().get(0); + assertNotNull(details.getErrors().get("type")); + } + +} diff --git a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java index 4ecbd4e37..66f350e0f 100644 --- a/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java +++ b/src/test/java/com/networknt/schema/PrefixItemsValidatorTest.java @@ -18,7 +18,7 @@ public class PrefixItemsValidatorTest extends AbstractJsonSchemaTestSuite { */ @Test void testEmptyPrefixItemsException() { - Stream dynamicNodeStream = createTests(SpecVersion.VersionFlag.V7, "src/test/suite/tests/prefixItemsException"); + Stream dynamicNodeStream = createTests(SpecVersion.VersionFlag.V7, "src/test/resources/prefixItemsException"); dynamicNodeStream.forEach( dynamicNode -> { assertThrows(JsonSchemaException.class, () -> { diff --git a/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java b/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java index 554e40971..aea1ca49b 100644 --- a/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java +++ b/src/test/java/com/networknt/schema/ThresholdMixinPerfTest.java @@ -330,7 +330,7 @@ public String thresholdValue() { }; private double getAvgTimeViaMixin(ThresholdMixin mixin, JsonNode value, int iterations) { - boolean excludeEqual = false; +// boolean excludeEqual = false; long totalTime = 0; for (int i = 0; i < iterations; i++) { long start = System.nanoTime(); diff --git a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java index 59397702a..1fc010276 100644 --- a/src/test/java/com/networknt/schema/V4JsonSchemaTest.java +++ b/src/test/java/com/networknt/schema/V4JsonSchemaTest.java @@ -45,13 +45,9 @@ public void testLoadingWithId() throws Exception { */ @Test public void testFailFast_AllErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-all-errors-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-all-errors-data.json"); + assertEquals(1, messages.size()); } /** @@ -59,13 +55,9 @@ public void testFailFast_AllErrors() throws IOException { */ @Test public void testFailFast_OneErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-one-error-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-one-error-data.json"); + assertEquals(1, messages.size()); } /** @@ -73,13 +65,9 @@ public void testFailFast_OneErrors() throws IOException { */ @Test public void testFailFast_TwoErrors() throws IOException { - try { - validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-two-errors-data.json"); - fail("Exception must be thrown"); - } catch (JsonSchemaException e) { - final Set messages = e.getValidationMessages(); - assertEquals(1, messages.size()); - } + Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + "extra/product/product-two-errors-data.json"); + assertEquals(1, messages.size()); } /** @@ -88,13 +76,9 @@ public void testFailFast_TwoErrors() throws IOException { */ @Test public void testFailFast_NoErrors() throws IOException { - try { - final Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", + final Set messages = validateFailingFastSchemaFor("extra/product/product.schema.json", "extra/product/product-no-errors-data.json"); - assertTrue(messages.isEmpty()); - } catch (JsonSchemaException e) { - fail("Must not get an errors"); - } + assertTrue(messages.isEmpty()); } private Set validateFailingFastSchemaFor(final String schemaFileName, final String dataFileName) throws IOException { diff --git a/src/test/java/com/networknt/schema/ValidationMessageTest.java b/src/test/java/com/networknt/schema/ValidationMessageTest.java new file mode 100644 index 000000000..1b3709d55 --- /dev/null +++ b/src/test/java/com/networknt/schema/ValidationMessageTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.networknt.schema.serialization.JsonMapperFactory; + +/** + * ValidationMessageTest. + */ +public class ValidationMessageTest { + @Test + void testSerialization() throws JsonProcessingException { + String value = JsonMapperFactory.getInstance() + .writeValueAsString(ValidationMessage.builder().messageSupplier(() -> "hello") + .schemaLocation(SchemaLocation.of("https://www.example.com/#defs/definition")).build()); + assertEquals("{\"message\":\"hello\",\"schemaLocation\":\"https://www.example.com/#defs/definition\"}", value); + } +} diff --git a/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java b/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java new file mode 100644 index 000000000..098ba11ae --- /dev/null +++ b/src/test/java/com/networknt/schema/output/OutputUnitDataTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024 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.output; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +/** + * OutputUnitDataTest. + */ +class OutputUnitDataTest { + + @Test + void format() { + String result = OutputUnitData.formatMessage("hello:"); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: "); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: "); + assertEquals("", result); + result = OutputUnitData.formatMessage("hello: world"); + assertEquals("world", result); + } +} diff --git a/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java b/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java new file mode 100644 index 000000000..a3178d237 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/MapSchemaLoaderTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2024 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.resource; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.AbsoluteIri; + +class MapSchemaLoaderTest { + public static class Result { + private final String schema; + + public Result(String schema) { + this.schema = schema; + } + + public String getSchema() { + return this.schema; + } + } + + @Test + void testMappingsWithTwoFunctions() throws IOException { + Map mappings = new HashMap<>(); + mappings.put("http://www.example.org/test.json", new Result("test")); + mappings.put("http://www.example.org/hello.json", new Result("hello")); + + MapSchemaLoader loader = new MapSchemaLoader(mappings::get, Result::getSchema); + InputStreamSource source = loader.getSchema(AbsoluteIri.of("http://www.example.org/test.json")); + try (InputStream inputStream = source.getInputStream()) { + byte[] r = new byte[4]; + inputStream.read(r); + String value = new String(r, StandardCharsets.UTF_8); + assertEquals("test", value); + } + + InputStreamSource result = loader.getSchema(AbsoluteIri.of("http://www.example.org/not-found.json")); + assertNull(result); + } +} diff --git a/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java b/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java new file mode 100644 index 000000000..d9697b6b1 --- /dev/null +++ b/src/test/java/com/networknt/schema/resource/MapSchemaMapperTest.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 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.resource; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.networknt.schema.AbsoluteIri; + +class MapSchemaMapperTest { + + @Test + void predicateMapping() { + MapSchemaMapper mapper = new MapSchemaMapper(test -> test.startsWith("http://www.example.org/"), + original -> original.replaceFirst("http://www.example.org/", "classpath:")); + AbsoluteIri result = mapper.map(AbsoluteIri.of("http://www.example.org/hello")); + assertEquals("classpath:hello", result.toString()); + result = mapper.map(AbsoluteIri.of("notmatchingprefixhttp://www.example.org/hello")); + assertNull(result); + } + +} diff --git a/src/test/suite/tests/prefixItemsException/prefixItemsException.json b/src/test/resources/prefixItemsException/prefixItemsException.json similarity index 100% rename from src/test/suite/tests/prefixItemsException/prefixItemsException.json rename to src/test/resources/prefixItemsException/prefixItemsException.json diff --git a/src/test/suite/tests/recursiveRefException/invalidRecursiveReference.json b/src/test/resources/recursiveRefException/invalidRecursiveReference.json similarity index 100% rename from src/test/suite/tests/recursiveRefException/invalidRecursiveReference.json rename to src/test/resources/recursiveRefException/invalidRecursiveReference.json diff --git a/src/test/resources/schema/id-relative.json b/src/test/resources/schema/id-relative.json new file mode 100644 index 000000000..d159f7ba7 --- /dev/null +++ b/src/test/resources/schema/id-relative.json @@ -0,0 +1,4 @@ +{ + "$id": "0", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} \ No newline at end of file diff --git a/src/test/resources/schema/issue936.json b/src/test/resources/schema/issue936.json new file mode 100644 index 000000000..d159f7ba7 --- /dev/null +++ b/src/test/resources/schema/issue936.json @@ -0,0 +1,4 @@ +{ + "$id": "0", + "$schema": "https://json-schema.org/draft/2020-12/schema" +} \ No newline at end of file diff --git a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json index dc7b2c56d..b5b3baab2 100644 --- a/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json +++ b/src/test/resources/schema/unevaluatedTests/unevaluated-tests.json @@ -415,8 +415,7 @@ }, "valid": false, "validationMessages": [ - "$.vehicle.unevaluated: must not have unevaluated properties", - "$.vehicle.wings: must not have unevaluated properties" + "$.vehicle.unevaluated: must not have unevaluated properties" ] } ] @@ -514,9 +513,7 @@ "valid": false, "validationMessages": [ "$.vehicle: required property 'wings' not found", - "$.vehicle.pontoons: must not have unevaluated properties", - "$.vehicle.unevaluated: must not have unevaluated properties", - "$.vehicle.wheels: must not have unevaluated properties" + "$.vehicle.unevaluated: must not have unevaluated properties" ] } ] diff --git a/src/test/suite/remotes/draft-next/detached-dynamicref.json b/src/test/suite/remotes/draft-next/detached-dynamicref.json new file mode 100644 index 000000000..c1a09a583 --- /dev/null +++ b/src/test/suite/remotes/draft-next/detached-dynamicref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft-next/detached-dynamicref.json", + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "foo": { + "$dynamicRef": "#detached" + }, + "detached": { + "$dynamicAnchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft-next/detached-ref.json b/src/test/suite/remotes/draft-next/detached-ref.json new file mode 100644 index 000000000..d01aaa128 --- /dev/null +++ b/src/test/suite/remotes/draft-next/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft-next/detached-ref.json", + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft-next/subSchemas.json b/src/test/suite/remotes/draft-next/subSchemas.json index 575dd00c2..75b7583ca 100644 --- a/src/test/suite/remotes/draft-next/subSchemas.json +++ b/src/test/suite/remotes/draft-next/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/next/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft2019-09/detached-ref.json b/src/test/suite/remotes/draft2019-09/detached-ref.json new file mode 100644 index 000000000..4a3499fd1 --- /dev/null +++ b/src/test/suite/remotes/draft2019-09/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft2019-09/detached-ref.json", + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft2019-09/subSchemas.json b/src/test/suite/remotes/draft2019-09/subSchemas.json index 6dea22525..fdfee68d9 100644 --- a/src/test/suite/remotes/draft2019-09/subSchemas.json +++ b/src/test/suite/remotes/draft2019-09/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2019-09/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft2020-12/subSchemas.json b/src/test/suite/remotes/draft2020-12/subSchemas.json index 5fca21d82..1bb4846d7 100644 --- a/src/test/suite/remotes/draft2020-12/subSchemas.json +++ b/src/test/suite/remotes/draft2020-12/subSchemas.json @@ -1,9 +1,11 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } } } diff --git a/src/test/suite/remotes/draft6/detached-ref.json b/src/test/suite/remotes/draft6/detached-ref.json new file mode 100644 index 000000000..05ce071ba --- /dev/null +++ b/src/test/suite/remotes/draft6/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft6/detached-ref.json", + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$id": "#detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/draft7/detached-ref.json b/src/test/suite/remotes/draft7/detached-ref.json new file mode 100644 index 000000000..27f2ec80a --- /dev/null +++ b/src/test/suite/remotes/draft7/detached-ref.json @@ -0,0 +1,13 @@ +{ + "$id": "http://localhost:1234/draft7/detached-ref.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$id": "#detached", + "type": "integer" + } + } +} \ No newline at end of file diff --git a/src/test/suite/remotes/subSchemas.json b/src/test/suite/remotes/subSchemas.json index 9f8030bce..6e9b3de35 100644 --- a/src/test/suite/remotes/subSchemas.json +++ b/src/test/suite/remotes/subSchemas.json @@ -1,8 +1,10 @@ { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/integer" + "definitions": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/definitions/integer" + } } } diff --git a/src/test/suite/tests/draft-next/anchor.json b/src/test/suite/tests/draft-next/anchor.json index 321d84461..a0c4c51a5 100644 --- a/src/test/suite/tests/draft-next/anchor.json +++ b/src/test/suite/tests/draft-next/anchor.json @@ -81,64 +81,6 @@ } ] }, - { - "description": "$anchor inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $anchor buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "anchor_in_enum": { - "enum": [ - { - "$anchor": "my_anchor", - "type": "null" - } - ] - }, - "real_identifier_in_schema": { - "$anchor": "my_anchor", - "type": "string" - }, - "zzz_anchor_in_const": { - "const": { - "$anchor": "my_anchor", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/anchor_in_enum" }, - { "$ref": "#my_anchor" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$anchor": "my_anchor", - "type": "null" - }, - "valid": true - }, - { - "description": "in implementations that strip $anchor, this may match either $def", - "data": { - "type": "null" - }, - "valid": false - }, - { - "description": "match $ref to $anchor", - "data": "a string to match #/$defs/anchor_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $anchor", - "data": 1, - "valid": false - } - ] - }, { "description": "same $anchor with different base uri", "schema": { @@ -175,38 +117,6 @@ } ] }, - { - "description": "non-schema object containing an $anchor property", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "const_not_anchor": { - "const": { - "$anchor": "not_a_real_anchor" - } - } - }, - "if": { - "const": "skip not_a_real_anchor" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_anchor" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_anchor", - "valid": true - }, - { - "description": "const at const_not_anchor does not match", - "data": 1, - "valid": false - } - ] - }, { "description": "invalid anchors", "schema": { diff --git a/src/test/suite/tests/draft-next/contains.json b/src/test/suite/tests/draft-next/contains.json index c17f55ee7..8539a531d 100644 --- a/src/test/suite/tests/draft-next/contains.json +++ b/src/test/suite/tests/draft-next/contains.json @@ -31,31 +31,6 @@ "data": [], "valid": false }, - { - "description": "object with property matching schema (5) is valid", - "data": { "a": 3, "b": 4, "c": 5 }, - "valid": true - }, - { - "description": "object with property matching schema (6) is valid", - "data": { "a": 3, "b": 4, "c": 6 }, - "valid": true - }, - { - "description": "object with two properties matching schema (5, 6) is valid", - "data": { "a": 3, "b": 4, "c": 5, "d": 6 }, - "valid": true - }, - { - "description": "object without properties matching schema is invalid", - "data": { "a": 2, "b": 3, "c": 4 }, - "valid": false - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false - }, { "description": "not array or object is valid", "data": 42, @@ -84,21 +59,6 @@ "description": "array without item 5 is invalid", "data": [1, 2, 3, 4], "valid": false - }, - { - "description": "object with property 5 is valid", - "data": { "a": 3, "b": 4, "c": 5 }, - "valid": true - }, - { - "description": "object with two properties 5 is valid", - "data": { "a": 3, "b": 4, "c": 5, "d": 5 }, - "valid": true - }, - { - "description": "object without property 5 is invalid", - "data": { "a": 1, "b": 2, "c": 3, "d": 4 }, - "valid": false } ] }, @@ -118,16 +78,6 @@ "description": "empty array is invalid", "data": [], "valid": false - }, - { - "description": "any non-empty object is valid", - "data": { "a": "foo" }, - "valid": true - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false } ] }, @@ -149,18 +99,28 @@ "valid": false }, { - "description": "any non-empty object is invalid", - "data": ["foo"], - "valid": false + "description": "non-arrays are valid - string", + "data": "contains does not apply to strings", + "valid": true }, { - "description": "empty object is invalid", + "description": "non-arrays are valid - object", "data": {}, - "valid": false + "valid": true }, { - "description": "non-arrays/objects are valid", - "data": "contains does not apply to strings", + "description": "non-arrays are valid - number", + "data": 42, + "valid": true + }, + { + "description": "non-arrays are valid - boolean", + "data": false, + "valid": true + }, + { + "description": "non-arrays are valid - null", + "data": null, "valid": true } ] @@ -193,26 +153,6 @@ "description": "matches neither items nor contains", "data": [1, 5], "valid": false - }, - { - "description": "matches additionalProperties, does not match contains", - "data": { "a": 2, "b": 4, "c": 8 }, - "valid": false - }, - { - "description": "does not match additionalProperties, matches contains", - "data": { "a": 3, "b": 6, "c": 9 }, - "valid": false - }, - { - "description": "matches both additionalProperties and contains", - "data": { "a": 6, "b": 12 }, - "valid": true - }, - { - "description": "matches neither additionalProperties nor contains", - "data": { "a": 1, "b": 5 }, - "valid": false } ] }, @@ -235,16 +175,6 @@ "description": "empty array is invalid", "data": [], "valid": false - }, - { - "description": "any non-empty object is valid", - "data": { "a": "foo" }, - "valid": true - }, - { - "description": "empty object is invalid", - "data": {}, - "valid": false } ] }, diff --git a/src/test/suite/tests/draft-next/dependentSchemas.json b/src/test/suite/tests/draft-next/dependentSchemas.json index 8a8477591..86079c34c 100644 --- a/src/test/suite/tests/draft-next/dependentSchemas.json +++ b/src/test/suite/tests/draft-next/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft-next/dynamicRef.json b/src/test/suite/tests/draft-next/dynamicRef.json index a4a7c4490..94124fff6 100644 --- a/src/test/suite/tests/draft-next/dynamicRef.json +++ b/src/test/suite/tests/draft-next/dynamicRef.json @@ -207,45 +207,75 @@ "schema": { "$schema": "https://json-schema.org/draft/next/schema", "$id": "https://test.json-schema.org/dynamic-ref-with-multiple-paths/main", - "$defs": { - "inner": { - "$id": "inner", - "$dynamicAnchor": "foo", - "title": "inner", - "additionalProperties": { - "$dynamicRef": "#foo" - } + "propertyDependencies": { + "kindOfList": { + "numbers": { "$ref": "numberList" }, + "strings": { "$ref": "stringList" } } }, - "if": { - "propertyNames": { - "pattern": "^[a-m]" + "$defs": { + "genericList": { + "$id": "genericList", + "properties": { + "list": { + "items": { "$dynamicRef": "#itemType" } + } + } + }, + "numberList": { + "$id": "numberList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "number" + } + }, + "$ref": "genericList" + }, + "stringList": { + "$id": "stringList", + "$defs": { + "itemType": { + "$dynamicAnchor": "itemType", + "type": "string" + } + }, + "$ref": "genericList" } - }, - "then": { - "title": "any type of node", - "$id": "anyLeafNode", - "$dynamicAnchor": "foo", - "$ref": "inner" - }, - "else": { - "title": "integer node", - "$id": "integerNode", - "$dynamicAnchor": "foo", - "type": [ "object", "integer" ], - "$ref": "inner" } }, "tests": [ { - "description": "recurse to anyLeafNode - floats are allowed", - "data": { "alpha": 1.1 }, + "description": "number list with number values", + "data": { + "kindOfList": "numbers", + "list": [1.1] + }, "valid": true }, { - "description": "recurse to integerNode - floats are not allowed", - "data": { "november": 1.1 }, + "description": "number list with string values", + "data": { + "kindOfList": "numbers", + "list": ["foo"] + }, + "valid": false + }, + { + "description": "string list with number values", + "data": { + "kindOfList": "strings", + "list": [1.1] + }, "valid": false + }, + { + "description": "string list with string values", + "data": { + "kindOfList": "strings", + "list": ["foo"] + }, + "valid": true } ] }, @@ -564,5 +594,53 @@ "valid": false } ] + }, + { + "description": "$ref to $dynamicRef finds detached $dynamicAnchor", + "schema": { + "$ref": "http://localhost:1234/draft-next/detached-dynamicref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "$dynamicRef points to a boolean schema", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "true": true, + "false": false + }, + "properties": { + "true": { + "$dynamicRef": "#/$defs/true" + }, + "false": { + "$dynamicRef": "#/$defs/false" + } + } + }, + "tests": [ + { + "description": "follow $dynamicRef to a true schema", + "data": { "true": 1 }, + "valid": true + }, + { + "description": "follow $dynamicRef to a false schema", + "data": { "false": 1 }, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/id.json b/src/test/suite/tests/draft-next/id.json index 9b3a591f0..fe74c6bff 100644 --- a/src/test/suite/tests/draft-next/id.json +++ b/src/test/suite/tests/draft-next/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft-next/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft-next/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft-next/items.json b/src/test/suite/tests/draft-next/items.json index 459943bef..dfb79af2f 100644 --- a/src/test/suite/tests/draft-next/items.json +++ b/src/test/suite/tests/draft-next/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft-next/maxContains.json b/src/test/suite/tests/draft-next/maxContains.json index 7c1515753..5af6e4c13 100644 --- a/src/test/suite/tests/draft-next/maxContains.json +++ b/src/test/suite/tests/draft-next/maxContains.json @@ -15,16 +15,6 @@ "description": "two items still valid against lone maxContains", "data": [1, 2], "valid": true - }, - { - "description": "one property valid against lone maxContains", - "data": { "a": 1 }, - "valid": true - }, - { - "description": "two properties still valid against lone maxContains", - "data": { "a": 1, "b": 2 }, - "valid": true } ] }, @@ -60,31 +50,6 @@ "description": "some elements match, invalid maxContains", "data": [1, 2, 1], "valid": false - }, - { - "description": "empty object", - "data": {}, - "valid": false - }, - { - "description": "all properties match, valid maxContains", - "data": { "a": 1 }, - "valid": true - }, - { - "description": "all properties match, invalid maxContains", - "data": { "a": 1, "b": 1 }, - "valid": false - }, - { - "description": "some properties match, valid maxContains", - "data": { "a": 1, "b": 2 }, - "valid": true - }, - { - "description": "some properties match, invalid maxContains", - "data": { "a": 1, "b": 2, "c": 1 }, - "valid": false } ] }, @@ -131,21 +96,6 @@ "description": "array with minContains < maxContains < actual", "data": [1, 1, 1, 1], "valid": false - }, - { - "description": "object with actual < minContains < maxContains", - "data": {}, - "valid": false - }, - { - "description": "object with minContains < actual < maxContains", - "data": { "a": 1, "b": 1 }, - "valid": true - }, - { - "description": "object with minContains < maxContains < actual", - "data": { "a": 1, "b": 1, "c": 1, "d": 1 }, - "valid": false } ] } diff --git a/src/test/suite/tests/draft-next/maxLength.json b/src/test/suite/tests/draft-next/maxLength.json index e09e44ad8..c88f604ef 100644 --- a/src/test/suite/tests/draft-next/maxLength.json +++ b/src/test/suite/tests/draft-next/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft-next/minLength.json b/src/test/suite/tests/draft-next/minLength.json index 16022acb5..52c9c9a14 100644 --- a/src/test/suite/tests/draft-next/minLength.json +++ b/src/test/suite/tests/draft-next/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft-next/optional/anchor.json b/src/test/suite/tests/draft-next/optional/anchor.json new file mode 100644 index 000000000..1de0b7a70 --- /dev/null +++ b/src/test/suite/tests/draft-next/optional/anchor.json @@ -0,0 +1,60 @@ +[ + { + "description": "$anchor inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $anchor buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "anchor_in_enum": { + "enum": [ + { + "$anchor": "my_anchor", + "type": "null" + } + ] + }, + "real_identifier_in_schema": { + "$anchor": "my_anchor", + "type": "string" + }, + "zzz_anchor_in_const": { + "const": { + "$anchor": "my_anchor", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/anchor_in_enum" }, + { "$ref": "#my_anchor" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$anchor": "my_anchor", + "type": "null" + }, + "valid": true + }, + { + "description": "in implementations that strip $anchor, this may match either $def", + "data": { + "type": "null" + }, + "valid": false + }, + { + "description": "match $ref to $anchor", + "data": "a string to match #/$defs/anchor_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $anchor", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft-next/optional/format/hostname.json b/src/test/suite/tests/draft-next/optional/format/hostname.json index 967848653..bfb306363 100644 --- a/src/test/suite/tests/draft-next/optional/format/hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/hostname.json @@ -95,6 +95,31 @@ "description": "exceeds maximum label length", "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", "valid": false + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json index ee2e792fa..109bf73c9 100644 --- a/src/test/suite/tests/draft-next/optional/format/idn-hostname.json +++ b/src/test/suite/tests/draft-next/optional/format/idn-hostname.json @@ -301,6 +301,31 @@ "comment": "https://tools.ietf.org/html/rfc5891#section-4.2.3.3 https://tools.ietf.org/html/rfc5892#appendix-A.1 https://www.w3.org/TR/alreq/#h_disjoining_enforcement", "data": "\u0628\u064a\u200c\u0628\u064a", "valid": true + }, + { + "description": "single label", + "data": "hostname", + "valid": true + }, + { + "description": "single label with hyphen", + "data": "host-name", + "valid": true + }, + { + "description": "single label with digits", + "data": "h0stn4me", + "valid": true + }, + { + "description": "single label starting with digit", + "data": "1host", + "valid": true + }, + { + "description": "single label ending with digit", + "data": "hostnam3", + "valid": true } ] } diff --git a/src/test/suite/tests/draft-next/optional/format/ipv4.json b/src/test/suite/tests/draft-next/optional/format/ipv4.json index e3e944015..2a4bc2b2f 100644 --- a/src/test/suite/tests/draft-next/optional/format/ipv4.json +++ b/src/test/suite/tests/draft-next/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft-next/optional/id.json b/src/test/suite/tests/draft-next/optional/id.json new file mode 100644 index 000000000..fc26f26c2 --- /dev/null +++ b/src/test/suite/tests/draft-next/optional/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "$id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an $id buried in the enum", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$defs": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/$defs/id_in_enum" }, + { "$ref": "https://localhost:1234/draft-next/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/draft-next/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to $id", + "data": "a string to match #/$defs/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to $id", + "data": 1, + "valid": false + } + ] + } +] diff --git a/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json b/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json index 489701cd2..c832e09f6 100644 --- a/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json +++ b/src/test/suite/tests/draft-next/optional/refOfUnknownKeyword.json @@ -42,5 +42,28 @@ "valid": false } ] + }, + { + "description": "reference internals of known non-applicator", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "/base", + "examples": [ + { "type": "string" } + ], + "$ref": "#/examples/0" + }, + "tests": [ + { + "description": "match", + "data": "a string", + "valid": true + }, + { + "description": "mismatch", + "data": 42, + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/unknownKeyword.json b/src/test/suite/tests/draft-next/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft-next/unknownKeyword.json rename to src/test/suite/tests/draft-next/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft-next/ref.json b/src/test/suite/tests/draft-next/ref.json index 1d5f25613..8417ce299 100644 --- a/src/test/suite/tests/draft-next/ref.json +++ b/src/test/suite/tests/draft-next/ref.json @@ -862,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,6 +888,7 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", @@ -909,6 +911,7 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", @@ -931,6 +934,7 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", @@ -953,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -982,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1006,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1030,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/next/schema", "$defs": { "": { "$defs": { diff --git a/src/test/suite/tests/draft-next/refRemote.json b/src/test/suite/tests/draft-next/refRemote.json index 3768b53b6..647fb9f19 100644 --- a/src/test/suite/tests/draft-next/refRemote.json +++ b/src/test/suite/tests/draft-next/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/next/schema", - "$ref": "http://localhost:1234/draft-next/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft-next/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -265,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -281,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -297,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -310,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$ref": "http://localhost:1234/draft-next/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft-next/unevaluatedItems.json b/src/test/suite/tests/draft-next/unevaluatedItems.json index 7379afb41..08f6ef128 100644 --- a/src/test/suite/tests/draft-next/unevaluatedItems.json +++ b/src/test/suite/tests/draft-next/unevaluatedItems.json @@ -461,6 +461,79 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "unevaluatedItems": false, + "prefixItems": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "prefixItems": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/unevaluated-items-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft-next/unevaluatedProperties.json b/src/test/suite/tests/draft-next/unevaluatedProperties.json index 69fe8a00c..d0d53507f 100644 --- a/src/test/suite/tests/draft-next/unevaluatedProperties.json +++ b/src/test/suite/tests/draft-next/unevaluatedProperties.json @@ -715,6 +715,92 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/next/schema", + "$id": "https://example.com/unevaluated-properties-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons" + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { @@ -1365,57 +1451,6 @@ } ] }, - { - "description": "unevaluatedProperties depends on adjacent contains", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "properties": { - "foo": { "type": "number" } - }, - "contains": { "type": "string" }, - "unevaluatedProperties": false - }, - "tests": [ - { - "description": "bar is evaluated by contains", - "data": { "foo": 1, "bar": "foo" }, - "valid": true - }, - { - "description": "contains fails, bar is not evaluated", - "data": { "foo": 1, "bar": 2 }, - "valid": false - }, - { - "description": "contains passes, bar is not evaluated", - "data": { "foo": 1, "bar": 2, "baz": "foo" }, - "valid": false - } - ] - }, - { - "description": "unevaluatedProperties depends on multiple nested contains", - "schema": { - "$schema": "https://json-schema.org/draft/next/schema", - "allOf": [ - { "contains": { "multipleOf": 2 } }, - { "contains": { "multipleOf": 3 } } - ], - "unevaluatedProperties": { "multipleOf": 5 } - }, - "tests": [ - { - "description": "5 not evaluated, passes unevaluatedItems", - "data": { "a": 2, "b": 3, "c": 4, "d": 5, "e": 6 }, - "valid": true - }, - { - "description": "7 not evaluated, fails unevaluatedItems", - "data": { "a": 2, "b": 3, "c": 4, "d": 7, "e": 8 }, - "valid": false - } - ] - }, { "description": "non-object instances are valid", "schema": { diff --git a/src/test/suite/tests/draft2019-09/additionalItems.json b/src/test/suite/tests/draft2019-09/additionalItems.json index aa44bcb76..9a7ae4f8a 100644 --- a/src/test/suite/tests/draft2019-09/additionalItems.json +++ b/src/test/suite/tests/draft2019-09/additionalItems.json @@ -182,6 +182,26 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft2019-09/dependentSchemas.json b/src/test/suite/tests/draft2019-09/dependentSchemas.json index 3577efdf4..c5b8ea05f 100644 --- a/src/test/suite/tests/draft2019-09/dependentSchemas.json +++ b/src/test/suite/tests/draft2019-09/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft2019-09/id.json b/src/test/suite/tests/draft2019-09/id.json index e2e403f0b..0ba313874 100644 --- a/src/test/suite/tests/draft2019-09/id.json +++ b/src/test/suite/tests/draft2019-09/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft2019-09/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft2019-09/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft2019-09/maxLength.json b/src/test/suite/tests/draft2019-09/maxLength.json index f242c3eff..a0cc7d9b8 100644 --- a/src/test/suite/tests/draft2019-09/maxLength.json +++ b/src/test/suite/tests/draft2019-09/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft2019-09/minLength.json b/src/test/suite/tests/draft2019-09/minLength.json index 19dec2cac..12782660c 100644 --- a/src/test/suite/tests/draft2019-09/minLength.json +++ b/src/test/suite/tests/draft2019-09/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft2019-09/not.json b/src/test/suite/tests/draft2019-09/not.json index af4df4c7a..d90728c7b 100644 --- a/src/test/suite/tests/draft2019-09/not.json +++ b/src/test/suite/tests/draft2019-09/not.json @@ -97,25 +97,173 @@ ] }, { - "description": "not with boolean schema true", + "description": "forbid everything with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": {} + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "not": true }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false } ] }, { - "description": "not with boolean schema false", + "description": "allow everything with boolean schema false", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "double negation", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "not": { "not": {} } + }, "tests": [ { "description": "any value is valid", @@ -146,9 +294,7 @@ { "description": "annotations are still collected inside a 'not'", "data": { "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: Annotations are not supported; only assertions are supported" + "valid": false } ] } diff --git a/src/test/suite/tests/draft2019-09/optional/format/ipv4.json b/src/test/suite/tests/draft2019-09/optional/format/ipv4.json index ac1e14c68..efe42471b 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/ipv4.json +++ b/src/test/suite/tests/draft2019-09/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft2019-09/optional/format/iri.json b/src/test/suite/tests/draft2019-09/optional/format/iri.json index 808c3c1eb..ad4c79e83 100644 --- a/src/test/suite/tests/draft2019-09/optional/format/iri.json +++ b/src/test/suite/tests/draft2019-09/optional/format/iri.json @@ -64,9 +64,7 @@ { "description": "an invalid IRI based on IPv6", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": false, - "disabled": true, - "reason": "URI syntax cannot always distinguish a malformed server-based authority from a legitimate registry-based authority" + "valid": false }, { "description": "an invalid relative IRI Reference", diff --git a/src/test/suite/tests/draft2019-09/recursiveRef.json b/src/test/suite/tests/draft2019-09/recursiveRef.json index 600b4a74d..22b47e749 100644 --- a/src/test/suite/tests/draft2019-09/recursiveRef.json +++ b/src/test/suite/tests/draft2019-09/recursiveRef.json @@ -348,8 +348,6 @@ "$ref": "recursiveRef8_inner.json" } }, - "disabled": true, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "recurse to anyLeafNode - floats are allowed", @@ -394,8 +392,6 @@ "$ref": "main.json#/$defs/inner" } }, - "disabled": true, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "numeric node", diff --git a/src/test/suite/tests/draft2019-09/ref.json b/src/test/suite/tests/draft2019-09/ref.json index 95a73345d..ea569908e 100644 --- a/src/test/suite/tests/draft2019-09/ref.json +++ b/src/test/suite/tests/draft2019-09/ref.json @@ -309,8 +309,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -475,8 +473,6 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -530,8 +526,6 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -589,8 +583,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -624,8 +616,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "data is valid against first definition", @@ -684,8 +674,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -830,8 +818,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -860,8 +846,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -878,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,8 +872,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -905,14 +888,13 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -929,14 +911,13 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -953,14 +934,13 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -975,111 +955,113 @@ ] }, { - "description": "ref with absolute-path-reference", - "schema": { - "$id": "http://example.com/ref/absref.json", - "$defs": { - "a": { - "$id": "http://example.com/ref/absref/foobar.json", - "type": "number" - }, - "b": { - "$id": "http://example.com/absref/foobar.json", - "type": "string" - } - }, - "$ref": "/absref/foobar.json" - }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", - "tests": [ - { - "description": "a string is valid", - "data": "foo", - "valid": true - }, - { - "description": "an integer is invalid", - "data": 12, - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - *nix", - "schema": { - "$id": "file:///folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "$id with file URI still resolves pointers - windows", - "schema": { - "$id": "file:///c:/folder/file.json", - "$defs": { - "foo": { - "type": "number" - } - }, - "$ref": "#/$defs/foo" - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - }, - { - "description": "empty tokens in $ref json-pointer", - "schema": { - "$defs": { - "": { - "$defs": { - "": { "type": "number" } - } - } - }, - "allOf": [ - { - "$ref": "#/$defs//$defs/" - } - ] - }, - "tests": [ - { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", - "valid": false - } - ] - } + "description": "ref with absolute-path-reference", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "http://example.com/ref/absref.json", + "$defs": { + "a": { + "$id": "http://example.com/ref/absref/foobar.json", + "type": "number" + }, + "b": { + "$id": "http://example.com/absref/foobar.json", + "type": "string" + } + }, + "$ref": "/absref/foobar.json" + }, + "tests": [ + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "an integer is invalid", + "data": 12, + "valid": false + } + ] + }, + { + "description": "$id with file URI still resolves pointers - *nix", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "$id with file URI still resolves pointers - windows", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "file:///c:/folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "empty tokens in $ref json-pointer", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "": { + "$defs": { + "": { "type": "number" } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs//$defs/" + } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + } ] diff --git a/src/test/suite/tests/draft2019-09/refRemote.json b/src/test/suite/tests/draft2019-09/refRemote.json index 00bf60b5b..072894cf2 100644 --- a/src/test/suite/tests/draft2019-09/refRemote.json +++ b/src/test/suite/tests/draft2019-09/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2019-09/schema", - "$ref": "http://localhost:1234/draft2019-09/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2019-09/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -113,8 +113,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -267,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -283,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -299,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -312,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$ref": "http://localhost:1234/draft2019-09/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2019-09/unevaluatedItems.json b/src/test/suite/tests/draft2019-09/unevaluatedItems.json index dc0615e09..8e2ee4b11 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedItems.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedItems.json @@ -212,9 +212,7 @@ { "description": "with invalid additional item", "data": ["yes", false], - "valid": false, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against 'false'." + "valid": false } ] }, @@ -310,9 +308,7 @@ { "description": "when two schemas match and has no unevaluated items", "data": ["foo", "bar", "baz"], - "valid": true, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against a string." + "valid": true }, { "description": "when two schemas match and has unevaluated items", @@ -484,6 +480,82 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "unevaluatedItems": false, + "items": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "items": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/unevaluated-items-with-recursive-ref/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "./tree", + "items": [ + true, + true, + { "type": "string" } + ], + + "$defs": { + "tree": { + "$id": "./tree", + "$recursiveAnchor": true, + + "type": "array", + "items": [ + { "type": "number" }, + { + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "$recursiveRef": "#" + } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [1, [2, [], "b"], "a"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": [1, [2, [], "b", "too many"], "a"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json index d75eab341..71c36dfa0 100644 --- a/src/test/suite/tests/draft2019-09/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2019-09/unevaluatedProperties.json @@ -358,9 +358,7 @@ "bar": "bar", "baz": "baz" }, - "valid": true, - "disabled": true, - "reason": "TODO: AnyOfValidator is short-circuiting" + "valid": true }, { "description": "when two match and has unevaluated properties", @@ -717,6 +715,102 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $recursiveRef", + "schema": { + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://example.com/unevaluated-properties-with-recursive-ref/extended-tree", + + "$recursiveAnchor": true, + + "$ref": "./tree", + "properties": { + "name": { "type": "string" } + }, + + "$defs": { + "tree": { + "$id": "./tree", + "$recursiveAnchor": true, + + "type": "object", + "properties": { + "node": true, + "branches": { + "$comment": "unevaluatedProperties comes first so it's more likely to bugs errors with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "$recursiveRef": "#" + } + }, + "required": ["node"] + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "name": "b", + "node": 2 + } + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "name": "a", + "node": 1, + "branches": { + "foo": "b", + "node": 2 + } + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { @@ -1328,9 +1422,7 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: unevaluatedProperties is not correct" + "valid": false }, { "description": "xx + a is invalid", diff --git a/src/test/suite/tests/draft2019-09/unknownKeyword.json b/src/test/suite/tests/draft2019-09/unknownKeyword.json deleted file mode 100644 index f98e87c54..000000000 --- a/src/test/suite/tests/draft2019-09/unknownKeyword.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "description": "$id inside an unknown keyword is not a real identifier", - "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", - "schema": { - "$schema": "https://json-schema.org/draft/2019-09/schema", - "$defs": { - "id_in_unknown0": { - "not": { - "array_of_schemas": [ - { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "null" - } - ] - } - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "string" - }, - "id_in_unknown1": { - "not": { - "object_of_schemas": { - "foo": { - "$id": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json", - "type": "integer" - } - } - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_unknown0" }, - { "$ref": "#/$defs/id_in_unknown1" }, - { "$ref": "https://localhost:1234/draft2019-09/unknownKeyword/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "type matches second anyOf, which has a real schema in it", - "data": "a string", - "valid": true - }, - { - "description": "type matches non-schema in first anyOf", - "data": null, - "valid": false - }, - { - "description": "type matches non-schema in third anyOf", - "data": 1, - "valid": false - } - ] - } -] diff --git a/src/test/suite/tests/draft2020-12/dependentSchemas.json b/src/test/suite/tests/draft2020-12/dependentSchemas.json index 66ac0eb43..1c5f0574a 100644 --- a/src/test/suite/tests/draft2020-12/dependentSchemas.json +++ b/src/test/suite/tests/draft2020-12/dependentSchemas.json @@ -132,6 +132,7 @@ { "description": "dependent subschema incompatible with root", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { "foo": {} }, diff --git a/src/test/suite/tests/draft2020-12/id.json b/src/test/suite/tests/draft2020-12/id.json index 0ae5fe68a..59265c4ec 100644 --- a/src/test/suite/tests/draft2020-12/id.json +++ b/src/test/suite/tests/draft2020-12/id.json @@ -207,88 +207,5 @@ "valid": true } ] - }, - { - "description": "$id inside an enum is not a real identifier", - "comment": "the implementation must not be confused by an $id buried in the enum", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "id_in_enum": { - "enum": [ - { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - } - ] - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "string" - }, - "zzz_id_in_const": { - "const": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_enum" }, - { "$ref": "https://localhost:1234/draft2020-12/id/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "exact match to enum, and type matches", - "data": { - "$id": "https://localhost:1234/draft2020-12/id/my_identifier.json", - "type": "null" - }, - "valid": true - }, - { - "description": "match $ref to $id", - "data": "a string to match #/$defs/id_in_enum", - "valid": true - }, - { - "description": "no match on enum or $ref to $id", - "data": 1, - "valid": false - } - ] - }, - { - "description": "non-schema object containing an $id property", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "const_not_id": { - "const": { - "$id": "not_a_real_id" - } - } - }, - "if": { - "const": "skip not_a_real_id" - }, - "then": true, - "else" : { - "$ref": "#/$defs/const_not_id" - } - }, - "tests": [ - { - "description": "skip traversing definition for a valid result", - "data": "skip not_a_real_id", - "valid": true - }, - { - "description": "const at const_not_id does not match", - "data": 1, - "valid": false - } - ] } ] diff --git a/src/test/suite/tests/draft2020-12/items.json b/src/test/suite/tests/draft2020-12/items.json index 1ef18bdd0..6a3e1cf26 100644 --- a/src/test/suite/tests/draft2020-12/items.json +++ b/src/test/suite/tests/draft2020-12/items.json @@ -265,6 +265,26 @@ } ] }, + { + "description": "items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [{}], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "items with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft2020-12/maxLength.json b/src/test/suite/tests/draft2020-12/maxLength.json index b6eb03401..7462726d7 100644 --- a/src/test/suite/tests/draft2020-12/maxLength.json +++ b/src/test/suite/tests/draft2020-12/maxLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft2020-12/minLength.json b/src/test/suite/tests/draft2020-12/minLength.json index e0930b6fb..5076c5a92 100644 --- a/src/test/suite/tests/draft2020-12/minLength.json +++ b/src/test/suite/tests/draft2020-12/minLength.json @@ -27,7 +27,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft2020-12/not.json b/src/test/suite/tests/draft2020-12/not.json index 5d5148277..d0f2b6e84 100644 --- a/src/test/suite/tests/draft2020-12/not.json +++ b/src/test/suite/tests/draft2020-12/not.json @@ -97,25 +97,173 @@ ] }, { - "description": "not with boolean schema true", + "description": "forbid everything with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": {} + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "not": true }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false } ] }, { - "description": "not with boolean schema false", + "description": "allow everything with boolean schema false", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "double negation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { "not": {} } + }, "tests": [ { "description": "any value is valid", @@ -146,9 +294,7 @@ { "description": "annotations are still collected inside a 'not'", "data": { "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: Annotations are not supported; only assertions are supported" + "valid": false } ] } diff --git a/src/test/suite/tests/draft2020-12/optional/format/ipv4.json b/src/test/suite/tests/draft2020-12/optional/format/ipv4.json index c72b6fc22..86d27bdb7 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/ipv4.json +++ b/src/test/suite/tests/draft2020-12/optional/format/ipv4.json @@ -81,6 +81,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft2020-12/optional/format/iri.json b/src/test/suite/tests/draft2020-12/optional/format/iri.json index 4b91f154e..311c9ef08 100644 --- a/src/test/suite/tests/draft2020-12/optional/format/iri.json +++ b/src/test/suite/tests/draft2020-12/optional/format/iri.json @@ -64,9 +64,7 @@ { "description": "an invalid IRI based on IPv6", "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", - "valid": false, - "disabled": true, - "reason": "URI syntax cannot always distinguish a malformed server-based authority from a legitimate registry-based authority" + "valid": false }, { "description": "an invalid relative IRI Reference", diff --git a/src/test/suite/tests/draft2020-12/ref.json b/src/test/suite/tests/draft2020-12/ref.json index 7ceb50e6e..8d15fa43a 100644 --- a/src/test/suite/tests/draft2020-12/ref.json +++ b/src/test/suite/tests/draft2020-12/ref.json @@ -309,8 +309,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -475,8 +473,6 @@ }, "$ref": "schema-relative-uri-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -530,8 +526,6 @@ }, "$ref": "schema-refs-absolute-uris-defs2.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -589,8 +583,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -624,8 +616,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "data is valid against first definition", @@ -684,8 +674,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -830,8 +818,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -860,8 +846,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -878,6 +862,7 @@ { "description": "URN ref with nested pointer ref", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", "$defs": { "foo": { @@ -887,8 +872,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -905,14 +888,13 @@ { "description": "ref to if", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/if", "if": { "$id": "http://example.com/ref/if", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -929,14 +911,13 @@ { "description": "ref to then", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/then", "then": { "$id": "http://example.com/ref/then", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -953,14 +934,13 @@ { "description": "ref to else", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": "http://example.com/ref/else", "else": { "$id": "http://example.com/ref/else", "type": "integer" } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -977,6 +957,7 @@ { "description": "ref with absolute-path-reference", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "http://example.com/ref/absref.json", "$defs": { "a": { @@ -990,8 +971,6 @@ }, "$ref": "/absref/foobar.json" }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -1008,6 +987,7 @@ { "description": "$id with file URI still resolves pointers - *nix", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///folder/file.json", "$defs": { "foo": { @@ -1032,6 +1012,7 @@ { "description": "$id with file URI still resolves pointers - windows", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "file:///c:/folder/file.json", "$defs": { "foo": { @@ -1056,6 +1037,7 @@ { "description": "empty tokens in $ref json-pointer", "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", "$defs": { "": { "$defs": { diff --git a/src/test/suite/tests/draft2020-12/refRemote.json b/src/test/suite/tests/draft2020-12/refRemote.json index 17c36a29a..047ac74ca 100644 --- a/src/test/suite/tests/draft2020-12/refRemote.json +++ b/src/test/suite/tests/draft2020-12/refRemote.json @@ -22,7 +22,7 @@ "description": "fragment within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/integer" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" }, "tests": [ { @@ -60,7 +60,7 @@ "description": "ref within remote ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas-defs.json#/$defs/refToInteger" + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" }, "tests": [ { @@ -113,8 +113,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -147,7 +145,6 @@ } } }, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -268,7 +265,10 @@ }, { "description": "remote HTTP ref with different $id", - "schema": {"$ref": "http://localhost:1234/different-id-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/different-id-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -284,7 +284,10 @@ }, { "description": "remote HTTP ref with different URN $id", - "schema": {"$ref": "http://localhost:1234/urn-ref-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/urn-ref-string.json" + }, "tests": [ { "description": "number is invalid", @@ -300,7 +303,10 @@ }, { "description": "remote HTTP ref with nested absolute ref", - "schema": {"$ref": "http://localhost:1234/nested-absolute-ref-to-string.json"}, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/nested-absolute-ref-to-string.json" + }, "tests": [ { "description": "number is invalid", @@ -313,5 +319,24 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft2020-12/unevaluatedItems.json b/src/test/suite/tests/draft2020-12/unevaluatedItems.json index 722b5dc03..ee0cb6586 100644 --- a/src/test/suite/tests/draft2020-12/unevaluatedItems.json +++ b/src/test/suite/tests/draft2020-12/unevaluatedItems.json @@ -191,9 +191,7 @@ { "description": "with invalid additional item", "data": ["yes", false], - "valid": false, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against 'false'." + "valid": false } ] }, @@ -289,9 +287,7 @@ { "description": "when two schemas match and has no unevaluated items", "data": ["foo", "bar", "baz"], - "valid": true, - "disabled": true, - "reason": "TODO: Is this a valid test? I don't see how 'true' validates against a string." + "valid": true }, { "description": "when two schemas match and has unevaluated items", @@ -465,6 +461,86 @@ } ] }, + { + "description": "unevaluatedItems before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "unevaluatedItems": false, + "prefixItems": [ + { "type": "string" } + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "prefixItems": [ + true, + { "type": "string" } + ] + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, + { + "description": "unevaluatedItems with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/unevaluated-items-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "prefixItems": [ + true, + { "type": "string" } + ] + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedItems comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedItems": false, + "type": "array", + "prefixItems": [ + { "type": "string" } + ], + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": ["foo", "bar"], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo", "bar", "baz"], + "valid": false + } + ] + }, { "description": "unevaluatedItems can't see inside cousins", "schema": { diff --git a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json index 83987ae6d..b8a2306ca 100644 --- a/src/test/suite/tests/draft2020-12/unevaluatedProperties.json +++ b/src/test/suite/tests/draft2020-12/unevaluatedProperties.json @@ -358,9 +358,7 @@ "bar": "bar", "baz": "baz" }, - "valid": true, - "disabled": true, - "reason": "TODO: AnyOfValidator is short-circuiting" + "valid": true }, { "description": "when two match and has unevaluated properties", @@ -717,6 +715,99 @@ } ] }, + { + "description": "unevaluatedProperties before $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "unevaluatedProperties": false, + "properties": { + "foo": { "type": "string" } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { "type": "string" } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties with $dynamicRef", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/unevaluated-properties-with-dynamic-ref/derived", + + "$ref": "./baseSchema", + + "$defs": { + "derived": { + "$dynamicAnchor": "addons", + "properties": { + "bar": { "type": "string" } + } + }, + "baseSchema": { + "$id": "./baseSchema", + + "$comment": "unevaluatedProperties comes first so it's more likely to catch bugs with implementations that are sensitive to keyword ordering", + "unevaluatedProperties": false, + "type": "object", + "properties": { + "foo": { "type": "string" } + }, + "$dynamicRef": "#addons", + + "$defs": { + "defaultAddons": { + "$comment": "Needed to satisfy the bookending requirement", + "$dynamicAnchor": "addons" + } + } + } + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true + }, + { + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false + } + ] + }, { "description": "unevaluatedProperties can't see inside cousins", "schema": { @@ -1328,9 +1419,7 @@ { "description": "xx + foo is invalid", "data": { "xx": 1, "foo": 1 }, - "valid": false, - "disabled": true, - "reason": "TODO: unevaluatedProperties is not correct" + "valid": false }, { "description": "xx + a is invalid", diff --git a/src/test/suite/tests/draft2020-12/unknownKeyword.json b/src/test/suite/tests/draft2020-12/unknownKeyword.json deleted file mode 100644 index 28b0c4ce9..000000000 --- a/src/test/suite/tests/draft2020-12/unknownKeyword.json +++ /dev/null @@ -1,57 +0,0 @@ -[ - { - "description": "$id inside an unknown keyword is not a real identifier", - "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "id_in_unknown0": { - "not": { - "array_of_schemas": [ - { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "null" - } - ] - } - }, - "real_id_in_schema": { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "string" - }, - "id_in_unknown1": { - "not": { - "object_of_schemas": { - "foo": { - "$id": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json", - "type": "integer" - } - } - } - } - }, - "anyOf": [ - { "$ref": "#/$defs/id_in_unknown0" }, - { "$ref": "#/$defs/id_in_unknown1" }, - { "$ref": "https://localhost:1234/draft2020-12/unknownKeyword/my_identifier.json" } - ] - }, - "tests": [ - { - "description": "type matches second anyOf, which has a real schema in it", - "data": "a string", - "valid": true - }, - { - "description": "type matches non-schema in first anyOf", - "data": null, - "valid": false - }, - { - "description": "type matches non-schema in third anyOf", - "data": 1, - "valid": false - } - ] - } -] diff --git a/src/test/suite/tests/draft3/additionalItems.json b/src/test/suite/tests/draft3/additionalItems.json index 0cb668701..ab44a2eb3 100644 --- a/src/test/suite/tests/draft3/additionalItems.json +++ b/src/test/suite/tests/draft3/additionalItems.json @@ -110,6 +110,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft3/maxLength.json b/src/test/suite/tests/draft3/maxLength.json index 4de42bcab..b0a9ea5be 100644 --- a/src/test/suite/tests/draft3/maxLength.json +++ b/src/test/suite/tests/draft3/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft3/minLength.json b/src/test/suite/tests/draft3/minLength.json index 3f09158de..6652c7509 100644 --- a/src/test/suite/tests/draft3/minLength.json +++ b/src/test/suite/tests/draft3/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft3/refRemote.json b/src/test/suite/tests/draft3/refRemote.json index de0cb43a5..0e4ab53e0 100644 --- a/src/test/suite/tests/draft3/refRemote.json +++ b/src/test/suite/tests/draft3/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { diff --git a/src/test/suite/tests/draft4/additionalItems.json b/src/test/suite/tests/draft4/additionalItems.json index deb44fd31..c9e681549 100644 --- a/src/test/suite/tests/draft4/additionalItems.json +++ b/src/test/suite/tests/draft4/additionalItems.json @@ -146,6 +146,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft4/maxLength.json b/src/test/suite/tests/draft4/maxLength.json index 811d35b25..338795943 100644 --- a/src/test/suite/tests/draft4/maxLength.json +++ b/src/test/suite/tests/draft4/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft4/minLength.json b/src/test/suite/tests/draft4/minLength.json index 3f09158de..6652c7509 100644 --- a/src/test/suite/tests/draft4/minLength.json +++ b/src/test/suite/tests/draft4/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft4/not.json b/src/test/suite/tests/draft4/not.json index cbb7f46bf..525219cf2 100644 --- a/src/test/suite/tests/draft4/not.json +++ b/src/test/suite/tests/draft4/not.json @@ -91,6 +91,67 @@ "valid": true } ] + }, + { + "description": "forbid everything with empty schema", + "schema": { "not": {} }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "double negation", + "schema": { "not": { "not": {} } }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] } - ] diff --git a/src/test/suite/tests/draft4/id.json b/src/test/suite/tests/draft4/optional/id.json similarity index 100% rename from src/test/suite/tests/draft4/id.json rename to src/test/suite/tests/draft4/optional/id.json diff --git a/src/test/suite/tests/draft4/ref.json b/src/test/suite/tests/draft4/ref.json index 4b170eb34..b53bd2abe 100644 --- a/src/test/suite/tests/draft4/ref.json +++ b/src/test/suite/tests/draft4/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -301,8 +299,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -436,8 +432,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -497,8 +491,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft4/refRemote.json b/src/test/suite/tests/draft4/refRemote.json index fb1d03cfe..64a618b89 100644 --- a/src/test/suite/tests/draft4/refRemote.json +++ b/src/test/suite/tests/draft4/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", diff --git a/src/test/suite/tests/draft6/additionalItems.json b/src/test/suite/tests/draft6/additionalItems.json index cae72361c..2c7d15582 100644 --- a/src/test/suite/tests/draft6/additionalItems.json +++ b/src/test/suite/tests/draft6/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft6/maxLength.json b/src/test/suite/tests/draft6/maxLength.json index 748b4daaf..be60c5407 100644 --- a/src/test/suite/tests/draft6/maxLength.json +++ b/src/test/suite/tests/draft6/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft6/minLength.json b/src/test/suite/tests/draft6/minLength.json index 64db94805..23c68fe3f 100644 --- a/src/test/suite/tests/draft6/minLength.json +++ b/src/test/suite/tests/draft6/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft6/not.json b/src/test/suite/tests/draft6/not.json index 98de0eda8..b46c4ed05 100644 --- a/src/test/suite/tests/draft6/not.json +++ b/src/test/suite/tests/draft6/not.json @@ -93,19 +93,161 @@ ] }, { - "description": "not with boolean schema true", - "schema": {"not": true}, + "description": "forbid everything with empty schema", + "schema": { "not": {} }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", + "schema": { "not": true }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "allow everything with boolean schema false", + "schema": { "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true } ] }, { - "description": "not with boolean schema false", - "schema": {"not": false}, + "description": "double negation", + "schema": { "not": { "not": {} } }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft6/id.json b/src/test/suite/tests/draft6/optional/id.json similarity index 100% rename from src/test/suite/tests/draft6/id.json rename to src/test/suite/tests/draft6/optional/id.json diff --git a/src/test/suite/tests/draft6/unknownKeyword.json b/src/test/suite/tests/draft6/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft6/unknownKeyword.json rename to src/test/suite/tests/draft6/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft6/ref.json b/src/test/suite/tests/draft6/ref.json index ed9fe56a5..379322c71 100644 --- a/src/test/suite/tests/draft6/ref.json +++ b/src/test/suite/tests/draft6/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -333,8 +331,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -449,6 +445,33 @@ } ] }, + { + "description": "Reference an anchor with a non-relative URI", + "schema": { + "$id": "https://example.com/schema-with-anchor", + "allOf": [{ + "$ref": "https://example.com/schema-with-anchor#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, { "description": "Location-independent identifier with base URI change in subschema", "schema": { @@ -468,8 +491,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -530,8 +551,6 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -584,8 +603,6 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -629,8 +646,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -755,8 +770,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -784,8 +797,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -817,8 +828,6 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft6/refRemote.json b/src/test/suite/tests/draft6/refRemote.json index 22baff6d3..28459c4a0 100644 --- a/src/test/suite/tests/draft6/refRemote.json +++ b/src/test/suite/tests/draft6/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -237,5 +235,23 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds location-independent $id", + "schema": { + "$ref": "http://localhost:1234/draft6/detached-ref.json#/definitions/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ] diff --git a/src/test/suite/tests/draft7/additionalItems.json b/src/test/suite/tests/draft7/additionalItems.json index cae72361c..2c7d15582 100644 --- a/src/test/suite/tests/draft7/additionalItems.json +++ b/src/test/suite/tests/draft7/additionalItems.json @@ -169,6 +169,25 @@ } ] }, + { + "description": "additionalItems with heterogeneous array", + "schema": { + "items": [{}], + "additionalItems": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ "foo", "bar", 37 ], + "valid": false + }, + { + "description": "valid instance", + "data": [ null ], + "valid": true + } + ] + }, { "description": "additionalItems with null instance elements", "schema": { diff --git a/src/test/suite/tests/draft7/maxLength.json b/src/test/suite/tests/draft7/maxLength.json index 748b4daaf..be60c5407 100644 --- a/src/test/suite/tests/draft7/maxLength.json +++ b/src/test/suite/tests/draft7/maxLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "two supplementary Unicode code points is long enough", + "description": "two graphemes is long enough", "data": "\uD83D\uDCA9\uD83D\uDCA9", "valid": true } diff --git a/src/test/suite/tests/draft7/minLength.json b/src/test/suite/tests/draft7/minLength.json index 64db94805..23c68fe3f 100644 --- a/src/test/suite/tests/draft7/minLength.json +++ b/src/test/suite/tests/draft7/minLength.json @@ -24,7 +24,7 @@ "valid": true }, { - "description": "one supplementary Unicode code point is not long enough", + "description": "one grapheme is not long enough", "data": "\uD83D\uDCA9", "valid": false } diff --git a/src/test/suite/tests/draft7/not.json b/src/test/suite/tests/draft7/not.json index 98de0eda8..b46c4ed05 100644 --- a/src/test/suite/tests/draft7/not.json +++ b/src/test/suite/tests/draft7/not.json @@ -93,19 +93,161 @@ ] }, { - "description": "not with boolean schema true", - "schema": {"not": true}, + "description": "forbid everything with empty schema", + "schema": { "not": {} }, "tests": [ { - "description": "any value is invalid", + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", "data": "foo", "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "forbid everything with boolean schema true", + "schema": { "not": true }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "allow everything with boolean schema false", + "schema": { "not": false }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true } ] }, { - "description": "not with boolean schema false", - "schema": {"not": false}, + "description": "double negation", + "schema": { "not": { "not": {} } }, "tests": [ { "description": "any value is valid", diff --git a/src/test/suite/tests/draft7/optional/format/ipv4.json b/src/test/suite/tests/draft7/optional/format/ipv4.json index 4706581f2..9680fe620 100644 --- a/src/test/suite/tests/draft7/optional/format/ipv4.json +++ b/src/test/suite/tests/draft7/optional/format/ipv4.json @@ -78,6 +78,11 @@ "description": "invalid non-ASCII '২' (a Bengali 2)", "data": "1২7.0.0.1", "valid": false + }, + { + "description": "netmask is not a part of ipv4 address", + "data": "192.168.1.0/24", + "valid": false } ] } diff --git a/src/test/suite/tests/draft7/id.json b/src/test/suite/tests/draft7/optional/id.json similarity index 100% rename from src/test/suite/tests/draft7/id.json rename to src/test/suite/tests/draft7/optional/id.json diff --git a/src/test/suite/tests/draft7/unknownKeyword.json b/src/test/suite/tests/draft7/optional/unknownKeyword.json similarity index 100% rename from src/test/suite/tests/draft7/unknownKeyword.json rename to src/test/suite/tests/draft7/optional/unknownKeyword.json diff --git a/src/test/suite/tests/draft7/ref.json b/src/test/suite/tests/draft7/ref.json index 82c1e8c24..82e1e1672 100644 --- a/src/test/suite/tests/draft7/ref.json +++ b/src/test/suite/tests/draft7/ref.json @@ -198,8 +198,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "$ref resolves to /definitions/base_foo, data does not validate", @@ -333,8 +331,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid tree", @@ -449,6 +445,33 @@ } ] }, + { + "description": "Reference an anchor with a non-relative URI", + "schema": { + "$id": "https://example.com/schema-with-anchor", + "allOf": [{ + "$ref": "https://example.com/schema-with-anchor#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, { "description": "Location-independent identifier with base URI change in subschema", "schema": { @@ -468,8 +491,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "data": 1, @@ -530,8 +551,6 @@ }, "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -584,8 +603,6 @@ }, "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "invalid on inner field", @@ -642,8 +659,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "number is valid", @@ -667,8 +682,6 @@ "foo": {"$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "valid under the URN IDed schema", @@ -793,8 +806,6 @@ "bar": {"type": "string"} } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -822,8 +833,6 @@ } } }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", @@ -850,8 +859,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -878,8 +885,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -906,8 +911,6 @@ } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a non-integer is invalid due to the $ref", @@ -939,8 +942,6 @@ { "$ref": "/absref/foobar.json" } ] }, - "disabled": false, - "reason": "Schema resources are currently unsupported. See #503", "tests": [ { "description": "a string is valid", diff --git a/src/test/suite/tests/draft7/refRemote.json b/src/test/suite/tests/draft7/refRemote.json index 22baff6d3..22185d678 100644 --- a/src/test/suite/tests/draft7/refRemote.json +++ b/src/test/suite/tests/draft7/refRemote.json @@ -17,7 +17,7 @@ }, { "description": "fragment within remote ref", - "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/definitions/integer"}, "tests": [ { "description": "remote fragment valid", @@ -34,7 +34,7 @@ { "description": "ref within remote ref", "schema": { - "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + "$ref": "http://localhost:1234/subSchemas.json#/definitions/refToInteger" }, "tests": [ { @@ -120,8 +120,6 @@ } } }, - "disabled": false, - "reason": "URI resolution does not account for identifiers that are not at the root schema", "tests": [ { "description": "number is valid", @@ -237,5 +235,23 @@ "valid": true } ] + }, + { + "description": "$ref to $ref finds location-independent $id", + "schema": { + "$ref": "http://localhost:1234/draft7/detached-ref.json#/definitions/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] } ]