Vert.x Json Schema provides an extendable and asynchronous implementation for Json Schema specification. You can use Json Schemas to validate every json structure. This module provides:
-
Implementation of Json Schema draft-7
-
Implementation of OpenAPI 3 dialect.
-
Non blocking
$refresolution and caching -
Lookup into the schema cache using
JsonPointer -
Synchronous and asynchronous validation
-
Ability to extend the validation tree adding new keywords and new format predicates
-
DSL to build schemas programmatically
Using Vert.x Json Schema
To use Vert.x Json Schema, add the following dependency to the dependencies section of your build descriptor:
-
Maven (in your
pom.xml):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-json-schema</artifactId>
<version>4.0.0-milestone5</version>
</dependency>
-
Gradle (in your
build.gradlefile):
dependencies {
compile 'io.vertx:vertx-json-schema:4.0.0-milestone5'
}
Concepts
Schema
SchemaParser & SchemaRouter
The SchemaParser is the component that parses the schemas from Json data structures to Schema instances.
The SchemaRouter is the component able to cache parsed schemas and resolve $ref`s.
Every time a new `$ref is solved or a SchemaParser parses a new schema, this will be cached inside the corresponding SchemaRouter.
The SchemaParser can be extended to support custom keywords and formats
Parse a schema
To parse a schema you first need a schema router and a schema parser matching your schema dialect. For example to instantiate a draft 7 schema parser:
var schemaRouter = SchemaRouter.create(vertx, SchemaRouterOptions())
var draft7SchemaParser = SchemaParser.createDraft7SchemaParser(schemaRouter)
You can reuse SchemaRouter instance for different SchemaParser`s and you can parse different `Schema`s with same `SchemaParser.
Now you can parse the schema:
var schema = parser.parse(`object`, schemaPointer)
When you parse a schema you must provide the schema pointer, a pointer that identifies the location of the schema.
If you don’t have any schema pointer SchemaParser will generate one for you:
var schema = parser.parse(`object`)
schema.getScope()
|
Important
|
Remember that the schema pointer is required to reference this schema later using Json Schema |
Validate
A schema could have two states:
-
Synchronous: The validators tree can provide a synchronous validation. You can validate your json both using
validateSyncandvalidateAsync</li> -
Asynchronous: One or more branches of the validator tree requires an asynchronous validation. You must use
validateAsyncto validate your json. If you usevalidateSyncit will throw aNoSyncValidationException</li>
To validate a schema in an asynchronous state:
schema.validateAsync(json).onComplete({ ar ->
if (ar.succeeded()) {
// Validation succeeded
} else {
// Validation failed
ar.cause()
}
})
To validate a schema in a synchronous state:
Code not translatable
To check the schema state you can use method isSync.
The schema can mutate the state in time, e.g. if you have a schema that is asynchronous because of a $ref,
after the first validation the external schema is cached and the schema will switch to synchronous state.
|
Note
|
If you use |
Apply default values
You can deeply apply default values to JsonObject and JsonArray:
schema.applyDefaultValues(jsonObject)
// Or
schema.applyDefaultValues(jsonArray)
These methods will mutate the internal state of the provided Json structures.
Adding custom formats
You can add custom formats to use with validation keyword format before parsing the schemas:
parser.withStringFormatValidator("firstUppercase", { str ->
Char.isUpperCase(str.charAt(0))
})
var mySchema = json {
obj("format" to "firstUppercase")
}
var schema = parser.parse(mySchema)
Adding custom keywords
For every new keyword type you want to provide, you must implement ValidatorFactory
and provide an instance to SchemaParser using withValidatorFactory.
When parsing happens, the SchemaParser calls canConsumeSchema for each registered factory.
If the factory can consume the schema, then the method createValidator
is called. This method returns an instance of Validator, that represents the object that will perform the validation.
If something goes wrong during Validator creation, a SchemaException should be thrown
You can add custom keywords of three types:
-
Keywords that always validate the input synchronously
-
Keywords that always validate the input asynchronously
-
Keywords with mutable state
Synchronous keywords
Synchronous validators must implement the interface SyncValidator.
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:
public class PropertiesMultipleOfValidator extends BaseSyncValidator {
private int multipleOf;
public PropertiesMultipleOfValidator(int multipleOf) {
this.multipleOf = multipleOf;
}
@Override
public void validateSync(Object in) throws ValidationException, NoSyncValidationException {
if (in instanceof JsonObject) { // If it's not an object, we skip the validation
if (((JsonObject)in).size() % multipleOf != 0) {
throw ValidationException
.createException(
"The provided object size is not a multiple of " + multipleOf,
"propertiesMultipleOf",
in
);
}
}
}
}
After we defined the keyword validator we can define the factory:
public class PropertiesMultipleOfValidatorFactory implements ValidatorFactory {
public final static String KEYWORD_NAME = "propertiesMultipleOf";
@Override
public Validator createValidator(JsonObject schema, JsonPointer scope, SchemaParserInternal parser, MutableStateValidator parent) {
try {
Number multipleOf = (Number) schema.getValue(KEYWORD_NAME);
return new PropertiesMultipleOfValidator(multipleOf.intValue());
} catch (ClassCastException e) {
throw new SchemaException(schema, "Wrong type for " + KEYWORD_NAME + " keyword", e);
} catch (NullPointerException e) {
throw new SchemaException(schema, "Null " + KEYWORD_NAME + " keyword", e);
}
}
@Override
public boolean canConsumeSchema(JsonObject schema) {
return schema.containsKey(KEYWORD_NAME);
}
}
Now we can mount the new validator factory:
parser.withValidatorFactory(examples.PropertiesMultipleOfValidatorFactory())
var mySchema = json {
obj("propertiesMultipleOf" to 2)
}
var schema = parser.parse(mySchema)
Asynchronous keywords
Synchronous validators must implement the interface AsyncValidator.
In this example I add a keyword that retrieves from the Vert.x Event bus an enum of values:
class AsyncEnumValidator extends BaseAsyncValidator {
private Vertx vertx;
private String address;
public AsyncEnumValidator(Vertx vertx, String address) {
this.vertx = vertx;
this.address = address;
}
@Override
public Future<Void> validateAsync(Object in) {
Promise<Void> promise = Promise.promise();
// Retrieve the valid values from the event bus
vertx.eventBus().request(address, new JsonObject(), ar -> {
JsonArray enumValues = (JsonArray) ar.result().body();
if (!enumValues.contains(in))
promise.fail(createException("Not matching async enum " + enumValues, "asyncEnum", in));
else
promise.complete();
});
return promise.future();
}
}
After we defined the keyword validator we can define the factory:
public class AsyncEnumValidatorFactory implements ValidatorFactory {
public final static String KEYWORD_NAME = "asyncEnum";
private Vertx vertx;
public AsyncEnumValidatorFactory(Vertx vertx) {
this.vertx = vertx;
}
@Override
public Validator createValidator(JsonObject schema, JsonPointer scope, SchemaParserInternal parser, MutableStateValidator parent) {
try {
String address = (String) schema.getValue(KEYWORD_NAME);
return new AsyncEnumValidator(vertx, address);
} catch (ClassCastException e) {
throw new SchemaException(schema, "Wrong type for " + KEYWORD_NAME + " keyword", e);
} catch (NullPointerException e) {
throw new SchemaException(schema, "Null " + KEYWORD_NAME + " keyword", e);
}
}
@Override
public boolean canConsumeSchema(JsonObject schema) {
return schema.containsKey(KEYWORD_NAME);
}
}
Now we can mount the new validator factory:
parser.withValidatorFactory(examples.AsyncEnumValidatorFactory(vertx))
var mySchema = json {
obj("asyncEnum" to "enums.myapplication")
}
var schema = parser.parse(mySchema)
Building your schemas from code
If you want to build schemas from code, you can use the included DSL. Only Draft-7 is supported for this feature.
Creating the schema
Inside Schemas there are static methods to create the schema:
Code not translatable
Using the keywords
For every schema you can add keywords built with Keywords methods,
depending on the type of the schema:
Code not translatable
Defining the schema structure
Depending on the schema you create, you can define a structure.
To create an object schema with some properties schemas and additional properties schema:
Code not translatable
To create an array schema:
Code not translatable
To create a tuple schema:
Code not translatable
$ref and aliases
To add a $ref schema you can use the Schemas.ref method.
To assign an $id keyword to a schema, use id
You can also refer to schemas defined with this dsl using aliases. You can use alias to assign an alias to
a schema. Then you can refer to a schema with an alias using Schemas.refToAlias:
Code not translatable
Using the schema
After you defined the schema, you can call build to parse and use the schema:
Code not translatable