Skip to content

Commit 9bd96a1

Browse files
authored
Merge pull request #56 from eclipse-vertx/issues/new-validator
Issues/new validator
2 parents 9e47d20 + d3f1dd7 commit 9bd96a1

File tree

66 files changed

+52607
-372
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+52607
-372
lines changed

src/main/asciidoc/index.adoc

Lines changed: 42 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44
Vert.x Json Schema provides an extendable and asynchronous implementation for https://json-schema.org/[Json Schema] specification.
55
You can use Json Schemas to validate every json structure. This module provides:
66

7-
* Implementation of https://tools.ietf.org/html/draft-handrews-json-schema-validation-02[Json Schema draft2019-09]
8-
* Implementation of https://tools.ietf.org/html/draft-handrews-json-schema-validation-01[Json Schema draft-7]
9-
* Implementation of https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject[OpenAPI 3 dialect].
10-
* Non blocking `$ref` resolution and caching
11-
* Lookup into the schema cache using {@link io.vertx.core.json.pointer.JsonPointer}
12-
* Synchronous and asynchronous validation
13-
* Ability to extend the validation tree adding new keywords and new format predicates
7+
* Implementation of https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00[draft 2020-12]
8+
* Implementation of https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-02[draft 2019-09]
9+
* Implementation of https://datatracker.ietf.org/doc/html/draft-handrews-json-schema-validation-01[draft 7]
10+
* Implementation of https://datatracker.ietf.org/doc/html/draft-fge-json-schema-validation-00[draft 4]
11+
* Dereferencing of `$ref` resolution and caching
1412
* DSL to build schemas programmatically
1513
1614
== Using Vert.x Json Schema
@@ -39,35 +37,45 @@ dependencies {
3937

4038
== Concepts
4139

42-
=== Schema
40+
=== JsonSchema
4341

44-
Each parsed schema is represented by a {@link io.vertx.json.schema.Schema} instance. A schema is a tree of {@link io.vertx.json.schema.common.Validator} objects,
45-
where each one contains the necessary logic to perform the validation. The performed validation is _fail-fast_: as soon as a validation error is encountered, the validation fails without going further
42+
Schemas can exist in 2 flavours:
4643

47-
=== SchemaParser & SchemaRouter
44+
* JSON as in JSON natation
45+
* Boolean as in `true/false`
4846

49-
The {@link io.vertx.json.schema.SchemaParser} is the component that parses the schemas from Json data structures to {@link io.vertx.json.schema.Schema} instances.
50-
The {@link io.vertx.json.schema.SchemaRouter} is the component able to cache parsed schemas and resolve `$ref`.
51-
Every time a new `$ref` is solved or a {@link io.vertx.json.schema.SchemaParser} parses a new schema, the new schema will be cached inside the corresponding {@link io.vertx.json.schema.SchemaRouter}.
52-
The {@link io.vertx.json.schema.SchemaParser} can be extended to support custom keywords and formats.
47+
The {@link io.vertx.json.schema.JsonSchema} interface allows both types to be handled without the constant check of the
48+
underlying type.
5349

54-
The available {@link io.vertx.json.schema.SchemaParser} are:
50+
=== SchemaRepository
5551

56-
* {@link io.vertx.json.schema.draft201909.Draft201909SchemaParser} for Json Schema Draft 2019-09
57-
* {@link io.vertx.json.schema.draft7.Draft7SchemaParser} for Json Schema Draft 7
58-
* {@link io.vertx.json.schema.openapi3.OpenAPI3SchemaParser} for OpenAPI 3 dialect
52+
The {@link io.vertx.json.schema.SchemaRepository} holds {@link io.vertx.json.schema.JsonSchema} instances. It performs
53+
dereferencing of schemas to speed up validation. The repository is a simple key store, this means that it does not allow
54+
duplicate ids.
55+
56+
The repository can then create {@link io.vertx.json.schema.Validator} instances aware of all sub schemas in the
57+
repository.
58+
59+
=== Validator
60+
61+
As the name implies the {@link io.vertx.json.schema.Validator} validates an object using a start schema. The output
62+
format is dependent of the configuration.
5963

6064
== Parse a schema
6165

62-
To parse a schema you first need a schema router and a schema parser matching your schema _dialect_.
63-
For example to instantiate a _draft 2019-09_ schema parser:
66+
When working with multiple schemas or sub-schemas, it is recommended to use a `Repository`.
67+
68+
To parse a schema you first need a `JsonSchema` and some initial configuration. Since schemas can contain references it
69+
is required for the validator and repository to be aware of your application `baseUri`. This allows you to reference your
70+
own schemas in other sub-schemas. For the purpose of dereferencing, you don't need to configure a draft.
6471

6572
[source,$lang]
6673
----
6774
{@link examples.JsonSchemaExamples#instantiate}
6875
----
6976

70-
You can reuse `SchemaRouter` instance for different `SchemaParser` and you can parse different `Schema` with same `SchemaParser`.
77+
You can use `JsonSchema` instances for different `Validator` and you can parse different `JsonSchema` with `JsonParser`
78+
directly.
7179

7280
Now you can parse the schema:
7381

@@ -76,123 +84,31 @@ Now you can parse the schema:
7684
{@link examples.JsonSchemaExamples#parse}
7785
----
7886

79-
When you parse a schema you must provide the **schema pointer**, a pointer that identifies the location of the schema.
80-
If you don't have any schema pointer `SchemaParser` will generate one for you:
81-
82-
[source,$lang]
83-
----
84-
{@link examples.JsonSchemaExamples#parseNoId}
85-
----
86-
8787
[IMPORTANT]
8888
====
89-
Remember that the schema pointer is required to reference this schema later using Json Schema `$ref`
90-
and to resolve relative references. If you load a schema from filesystem and you use relative references, **provide the correct pointer** or the
91-
`SchemaRouter` won't be able to resolve the local filesystem `$ref`.
89+
Remember that for security reasons, this module will not attempt to download any referenced sub-schema. All required
90+
sub-schemas should be provided to a repository object.
9291
====
9392

9493
== Validate
9594

95+
Given the dynamic nature of json-schema and the conditional `if-then-else` it is not possible to validate in a streaming
96+
scenario. Validation is for this reason a blocking operation. If you are aware that validation will be a very expensive
97+
process, then it is advisable to run the validation on a dedicated thread pool or using `executeBlocking`.
9698
A schema could have two states:
9799

98-
* Synchronous: The validators tree can provide a synchronous validation. You can validate your json both using {@link io.vertx.json.schema.Schema#validateSync(Object)} and {@link io.vertx.json.schema.Schema#validateAsync(Object)}
99-
* Asynchronous: One or more branches of the validator tree requires an asynchronous validation. You must use {@link io.vertx.json.schema.Schema#validateAsync(Object)} to validate your json. If you use {@link io.vertx.json.schema.Schema#validateSync(Object)} it will throw a {@link io.vertx.json.schema.NoSyncValidationException}
100-
101-
To validate a schema in an asynchronous state:
102-
103-
[source,$lang]
104-
----
105-
{@link examples.JsonSchemaExamples#validateAsync}
106-
----
107-
108-
To validate a schema in a synchronous state:
100+
To validate a schema:
109101

110102
[source,$lang]
111103
----
112-
{@link examples.JsonSchemaExamples#validateSync}
104+
{@link examples.JsonSchemaExamples#validate}
113105
----
114106

115-
To check the schema state you can use method {@link io.vertx.json.schema.Schema#isSync()}.
116-
The schema can mutate the state in time, e.g. if you have a schema that is asynchronous because of a `$ref`,
117-
after the first validation the external schema is cached and the schema will switch to synchronous state.
118-
119-
[NOTE]
120-
====
121-
If you use {@link io.vertx.json.schema.Schema#validateAsync(Object)} while the schema is in a synchronous state,
122-
the schema will validate synchronously wrapping the result in the returned `Future`, avoiding unnecessary async computations and memory usage
123-
====
124-
125-
== Adding custom formats
126-
127-
You can add custom formats to use with validation keyword `format` before parsing the schemas:
128-
129-
[source,$lang]
130-
----
131-
{@link examples.JsonSchemaExamples#customFormat}
132-
----
133-
134-
== Adding custom keywords
135-
136-
For every new keyword type you want to provide, you must implement {@link io.vertx.json.schema.common.ValidatorFactory}
137-
and provide an instance to `SchemaParser` using {@link io.vertx.json.schema.SchemaParser#withValidatorFactory(ValidatorFactory)}.
138-
When parsing happens, the `SchemaParser` calls {@link io.vertx.json.schema.common.ValidatorFactory#canConsumeSchema(JsonObject)} for each registered factory.
139-
If the factory can consume the schema, then the method {@link io.vertx.json.schema.common.ValidatorFactory#createValidator(JsonObject, JsonPointer, SchemaParserInternal, MutableStateValidator)}
140-
is called. This method returns an instance of {@link io.vertx.json.schema.common.Validator}, that represents the object that will perform the validation.
141-
If something goes wrong during `Validator` creation, a {@link io.vertx.json.schema.SchemaException} should be thrown
142-
143-
You can add custom keywords of three types:
144-
145-
* Keywords that always validate the input synchronously
146-
* Keywords that always validate the input asynchronously
147-
* Keywords with mutable state
148-
149-
=== Synchronous keywords
150-
151-
Synchronous validators must implement the interface {@link io.vertx.json.schema.common.SyncValidator}.
152-
In the example below I add a keyword that checks if the number of properties in a json object is a multiple of a provided number:
107+
== Custom formats
153108

154-
[source,$lang]
155-
----
156-
{@link examples.PropertiesMultipleOfValidator}
157-
----
158-
159-
After we defined the keyword validator we can define the factory:
160-
161-
[source,$lang]
162-
----
163-
{@link examples.PropertiesMultipleOfValidatorFactory}
164-
----
165-
166-
Now we can mount the new validator factory:
167-
168-
[source,$lang]
169-
----
170-
{@link examples.JsonSchemaExamples#mountSyncKeyword}
171-
----
172-
173-
=== Asynchronous keywords
174-
175-
Asynchronous validators must implement the interface {@link io.vertx.json.schema.common.AsyncValidator}.
176-
In this example I add a keyword that retrieves from the Vert.x Event bus an enum of values:
177-
178-
[source,$lang]
179-
----
180-
{@link examples.AsyncEnumValidator}
181-
----
182-
183-
After we defined the keyword validator we can define the factory:
184-
185-
[source,$lang]
186-
----
187-
{@link examples.AsyncEnumValidatorFactory}
188-
----
189-
190-
Now we can mount the new validator factory:
191-
192-
[source,$lang]
193-
----
194-
{@link examples.JsonSchemaExamples#mountAsyncKeyword}
195-
----
109+
Adding custom formats is not allowed. This may sound like a limitation but it arises from the validation spec.
110+
Validators are expected to assume unknown formats as valid input, delegating to the application business code the role
111+
of validator for custom values.
196112

197113
== Building your schemas from code
198114

@@ -259,7 +175,7 @@ a schema. Then you can refer to a schema with an alias using {@link io.vertx.jso
259175

260176
=== Using the schema
261177

262-
After you defined the schema, you can call {@link io.vertx.json.schema.common.dsl.SchemaBuilder#build(SchemaParser)} to parse and use the schema:
178+
After you defined the schema, you can call {@link io.vertx.json.schema.common.dsl.SchemaBuilder#toJson()} to return the JSON notation of the schema:
263179

264180
[source,$lang]
265181
----
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package io.vertx.json.schema;
2+
3+
import io.vertx.core.json.JsonObject;
4+
import io.vertx.core.json.JsonArray;
5+
import io.vertx.core.json.impl.JsonUtil;
6+
import java.time.Instant;
7+
import java.time.format.DateTimeFormatter;
8+
import java.util.Base64;
9+
10+
/**
11+
* Converter and mapper for {@link io.vertx.json.schema.JsonSchemaOptions}.
12+
* NOTE: This class has been automatically generated from the {@link io.vertx.json.schema.JsonSchemaOptions} original class using Vert.x codegen.
13+
*/
14+
public class JsonSchemaOptionsConverter {
15+
16+
17+
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
18+
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
19+
20+
public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, JsonSchemaOptions obj) {
21+
for (java.util.Map.Entry<String, Object> member : json) {
22+
switch (member.getKey()) {
23+
case "baseUri":
24+
if (member.getValue() instanceof String) {
25+
obj.setBaseUri((String)member.getValue());
26+
}
27+
break;
28+
case "draft":
29+
if (member.getValue() instanceof String) {
30+
obj.setDraft(io.vertx.json.schema.Draft.valueOf((String)member.getValue()));
31+
}
32+
break;
33+
case "outputFormat":
34+
if (member.getValue() instanceof String) {
35+
obj.setOutputFormat(io.vertx.json.schema.OutputFormat.valueOf((String)member.getValue()));
36+
}
37+
break;
38+
}
39+
}
40+
}
41+
42+
public static void toJson(JsonSchemaOptions obj, JsonObject json) {
43+
toJson(obj, json.getMap());
44+
}
45+
46+
public static void toJson(JsonSchemaOptions obj, java.util.Map<String, Object> json) {
47+
if (obj.getBaseUri() != null) {
48+
json.put("baseUri", obj.getBaseUri());
49+
}
50+
if (obj.getDraft() != null) {
51+
json.put("draft", obj.getDraft().name());
52+
}
53+
if (obj.getOutputFormat() != null) {
54+
json.put("outputFormat", obj.getOutputFormat().name());
55+
}
56+
}
57+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.vertx.json.schema;
2+
3+
import io.vertx.core.json.JsonObject;
4+
import io.vertx.core.json.JsonArray;
5+
import io.vertx.core.json.impl.JsonUtil;
6+
import java.time.Instant;
7+
import java.time.format.DateTimeFormatter;
8+
import java.util.Base64;
9+
10+
/**
11+
* Converter and mapper for {@link io.vertx.json.schema.OutputUnit}.
12+
* NOTE: This class has been automatically generated from the {@link io.vertx.json.schema.OutputUnit} original class using Vert.x codegen.
13+
*/
14+
public class OutputUnitConverter {
15+
16+
17+
private static final Base64.Decoder BASE64_DECODER = JsonUtil.BASE64_DECODER;
18+
private static final Base64.Encoder BASE64_ENCODER = JsonUtil.BASE64_ENCODER;
19+
20+
public static void fromJson(Iterable<java.util.Map.Entry<String, Object>> json, OutputUnit obj) {
21+
for (java.util.Map.Entry<String, Object> member : json) {
22+
switch (member.getKey()) {
23+
case "annotations":
24+
if (member.getValue() instanceof JsonArray) {
25+
java.util.ArrayList<io.vertx.json.schema.OutputUnit> list = new java.util.ArrayList<>();
26+
((Iterable<Object>)member.getValue()).forEach( item -> {
27+
if (item instanceof JsonObject)
28+
list.add(new io.vertx.json.schema.OutputUnit((io.vertx.core.json.JsonObject)item));
29+
});
30+
obj.setAnnotations(list);
31+
}
32+
break;
33+
case "error":
34+
if (member.getValue() instanceof String) {
35+
obj.setError((String)member.getValue());
36+
}
37+
break;
38+
case "errors":
39+
if (member.getValue() instanceof JsonArray) {
40+
java.util.ArrayList<io.vertx.json.schema.OutputUnit> list = new java.util.ArrayList<>();
41+
((Iterable<Object>)member.getValue()).forEach( item -> {
42+
if (item instanceof JsonObject)
43+
list.add(new io.vertx.json.schema.OutputUnit((io.vertx.core.json.JsonObject)item));
44+
});
45+
obj.setErrors(list);
46+
}
47+
break;
48+
case "instanceLocation":
49+
if (member.getValue() instanceof String) {
50+
obj.setInstanceLocation((String)member.getValue());
51+
}
52+
break;
53+
case "keyword":
54+
if (member.getValue() instanceof String) {
55+
obj.setKeyword((String)member.getValue());
56+
}
57+
break;
58+
case "keywordLocation":
59+
if (member.getValue() instanceof String) {
60+
obj.setKeywordLocation((String)member.getValue());
61+
}
62+
break;
63+
case "valid":
64+
if (member.getValue() instanceof Boolean) {
65+
obj.setValid((Boolean)member.getValue());
66+
}
67+
break;
68+
}
69+
}
70+
}
71+
72+
public static void toJson(OutputUnit obj, JsonObject json) {
73+
toJson(obj, json.getMap());
74+
}
75+
76+
public static void toJson(OutputUnit obj, java.util.Map<String, Object> json) {
77+
if (obj.getAnnotations() != null) {
78+
JsonArray array = new JsonArray();
79+
obj.getAnnotations().forEach(item -> array.add(item.toJson()));
80+
json.put("annotations", array);
81+
}
82+
if (obj.getError() != null) {
83+
json.put("error", obj.getError());
84+
}
85+
if (obj.getErrors() != null) {
86+
JsonArray array = new JsonArray();
87+
obj.getErrors().forEach(item -> array.add(item.toJson()));
88+
json.put("errors", array);
89+
}
90+
if (obj.getInstanceLocation() != null) {
91+
json.put("instanceLocation", obj.getInstanceLocation());
92+
}
93+
if (obj.getKeyword() != null) {
94+
json.put("keyword", obj.getKeyword());
95+
}
96+
if (obj.getKeywordLocation() != null) {
97+
json.put("keywordLocation", obj.getKeywordLocation());
98+
}
99+
if (obj.getValid() != null) {
100+
json.put("valid", obj.getValid());
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)