Vert.x Json Schema

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 draft2019-09

  • Implementation of Json Schema draft-7

  • Implementation of OpenAPI 3 dialect.

  • Non blocking $ref resolution 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.CR2</version>
</dependency>
  • Gradle (in your build.gradle file):

dependencies {
 compile 'io.vertx:vertx-json-schema:4.0.0.CR2'
}

Concepts

Schema

Each parsed schema is represented by a Schema instance. A schema is a tree of Validator objects, 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

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. Every time a new $ref is solved or a SchemaParser parses a new schema, the new schema will be cached inside the corresponding SchemaRouter. The SchemaParser can be extended to support custom keywords and formats.

The available SchemaParser are:

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 2019-09 schema parser:

SchemaRouter schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions());
SchemaParser schemaParser = SchemaParser.createDraft201909SchemaParser(schemaRouter);

You can reuse SchemaRouter instance for different SchemaParser and you can parse different Schema with same SchemaParser.

Now you can parse the schema:

Schema 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:

Schema schema = parser.parse(object);
schema.getScope(); // Get generated scope of schema (schema pointer)
Important

Remember that the schema pointer is required to reference this schema later using Json Schema $ref and to resolve relative references. If you load a schema from filesystem and you use relative references, provide the correct pointer or the SchemaRouter won’t be able to resolve the local filesystem $ref.

Validate

A schema could have two states:

To validate a schema in an asynchronous state:

schema.validateAsync(json).onComplete(ar -> {
  if (ar.succeeded()) {
    // Validation succeeded
  } else {
    // Validation failed
    ar.cause(); // Contains ValidationException
  }
});

To validate a schema in a synchronous state:

try {
  schema.validateSync(json);
  // Successful validation
} catch (ValidationException e) {
  // Failed validation
} catch (NoSyncValidationException e) {
  // Cannot validate synchronously. You must validate using validateAsync
}

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 validateAsync while the schema is in a synchronous state, the schema will validate synchronously wrapping the result in the returned Future, avoiding unnecessary async computations and memory usage

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 -> Character.isUpperCase(str.charAt(0)));

JsonObject mySchema = new JsonObject().put("format", "firstUppercase");
Schema 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:

`link:../../apidocs/examples/PropertiesMultipleOfValidator.html[PropertiesMultipleOfValidator]`

After we defined the keyword validator we can define the factory:

`link:../../apidocs/examples/PropertiesMultipleOfValidatorFactory.html[PropertiesMultipleOfValidatorFactory]`

Now we can mount the new validator factory:

parser.withValidatorFactory(new PropertiesMultipleOfValidatorFactory());

JsonObject mySchema = new JsonObject().put("propertiesMultipleOf", 2);
Schema 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:

`link:../../apidocs/examples/AsyncEnumValidator.html[AsyncEnumValidator]`

After we defined the keyword validator we can define the factory:

`link:../../apidocs/examples/AsyncEnumValidatorFactory.html[AsyncEnumValidatorFactory]`

Now we can mount the new validator factory:

parser.withValidatorFactory(new AsyncEnumValidatorFactory(vertx));

JsonObject mySchema = new JsonObject().put("asyncEnum", "enums.myapplication");
Schema 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.

To start, add static imports for Schemas and Keywords

Creating the schema

Inside Schemas there are static methods to create the schema:

SchemaBuilder intSchemaBuilder = intSchema();
SchemaBuilder objectSchemaBuilder = objectSchema();

Using the keywords

For every schema you can add keywords built with Keywords methods, depending on the type of the schema:

stringSchema()
  .with(format(StringFormat.DATETIME));
arraySchema()
  .with(maxItems(10));
schema() // Generic schema that accepts both arrays and integers
  .with(type(SchemaType.ARRAY, SchemaType.INT));

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:

objectSchema()
  .requiredProperty("name", stringSchema())
  .requiredProperty("age", intSchema())
  .additionalProperties(stringSchema());

To create an array schema:

arraySchema()
  .items(stringSchema());

To create a tuple schema:

tupleSchema()
  .item(stringSchema()) // First item
  .item(intSchema()) // Second item
  .item(booleanSchema()); // Third item

$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:

intSchema()
  .alias("myInt");

objectSchema()
  .requiredProperty("anInteger", refToAlias("myInt"));

Using the schema

After you defined the schema, you can call build to parse and use the schema:

Schema schema = objectSchema()
  .requiredProperty("name", stringSchema())
  .requiredProperty("age", intSchema())
  .additionalProperties(stringSchema())
  .build(parser);