# 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.Beta2</version>
    </dependency>

- Gradle (in your `build.gradle` file):

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

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

- `Draft201909SchemaParser` for Json Schema Draft 2019-09

- `Draft7SchemaParser` for Json Schema Draft 7

- `OpenAPI3SchemaParser` for OpenAPI 3 dialect

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

- Synchronous: The validators tree can provide a synchronous validation. You can validate your json both using `validateSync` and `validateAsync`</li>

- Asynchronous: One or more branches of the validator tree requires an asynchronous validation. You must use `validateAsync` to validate your json. If you use `validateSync` it will throw a `NoSyncValidationException`</li>

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);