= Vert.x Json Schema
:toc: left

Vert.x Json Schema provides an extendable and asynchronous implementation for https://json-schema.org/[Json Schema] specification.
You can use Json Schemas to validate every json structure. This module provides:

* Implementation of https://tools.ietf.org/html/draft-handrews-json-schema-validation-02[Json Schema draft2019-09]
* Implementation of https://tools.ietf.org/html/draft-handrews-json-schema-validation-01[Json Schema draft-7]
* Implementation of https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md#schemaObject[OpenAPI 3 dialect].
* Non blocking `$ref` resolution and caching
* Lookup into the schema cache using `link:../../apidocs/io/vertx/core/json/pointer/JsonPointer.html[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`):

[source,xml,subs="+attributes"]
----
<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-json-schema</artifactId>
 <version>4.0.0.Beta1</version>
</dependency>
----

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

[source,groovy,subs="+attributes"]
----
dependencies {
 compile 'io.vertx:vertx-json-schema:4.0.0.Beta1'
}
----

== Concepts

=== Schema

Each parsed schema is represented by a `link:../../apidocs/io/vertx/ext/json/schema/Schema.html[Schema]` instance. A schema is a tree of `link:../../apidocs/io/vertx/ext/json/schema/common/Validator.html[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 `link:../../apidocs/io/vertx/ext/json/schema/SchemaParser.html[SchemaParser]` is the component that parses the schemas from Json data structures to `link:../../apidocs/io/vertx/ext/json/schema/Schema.html[Schema]` instances.
The `link:../../apidocs/io/vertx/ext/json/schema/SchemaRouter.html[SchemaRouter]` is the component able to cache parsed schemas and resolve `$ref`.
Every time a new `$ref` is solved or a `link:../../apidocs/io/vertx/ext/json/schema/SchemaParser.html[SchemaParser]` parses a new schema, the new schema will be cached inside the corresponding `link:../../apidocs/io/vertx/ext/json/schema/SchemaRouter.html[SchemaRouter]`.
The `link:../../apidocs/io/vertx/ext/json/schema/SchemaParser.html[SchemaParser]` can be extended to support custom keywords and formats.

The available `link:../../apidocs/io/vertx/ext/json/schema/SchemaParser.html[SchemaParser]` are:

* `link:../../apidocs/io/vertx/ext/json/schema/draft201909/Draft201909SchemaParser.html[Draft201909SchemaParser]` for Json Schema Draft 2019-09
* `link:../../apidocs/io/vertx/ext/json/schema/draft7/Draft7SchemaParser.html[Draft7SchemaParser]` for Json Schema Draft 7
* `link:../../apidocs/io/vertx/ext/json/schema/openapi3/OpenAPI3SchemaParser.html[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:

[source,groovy]
----
def schemaRouter = SchemaRouter.create(vertx, [:])
def 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:

[source,groovy]
----
def 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:

[source,groovy]
----
def schema = parser.parse(object)
schema.getScope()

----

[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 `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#validateSync-java.lang.Object-[validateSync]` and `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#validateAsync-java.lang.Object-[validateAsync]`</li>
* Asynchronous: One or more branches of the validator tree requires an asynchronous validation. You must use `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#validateAsync-java.lang.Object-[validateAsync]` to validate your json. If you use `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#validateSync-java.lang.Object-[validateSync]` it will throw a `link:../../apidocs/io/vertx/ext/json/schema/NoSyncValidationException.html[NoSyncValidationException]`</li>

To validate a schema in an asynchronous state:

[source,groovy]
----
schema.validateAsync(json).onComplete({ ar ->
  if (ar.succeeded()) {
    // Validation succeeded
  } else {
    // Validation failed
    ar.cause()
  }
})

----

To validate a schema in a synchronous state:

[source,groovy]
----
Code not translatable
----

To check the schema state you can use method `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#isSync--[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 `link:../../apidocs/io/vertx/ext/json/schema/Schema.html#validateAsync-java.lang.Object-[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`:

[source,groovy]
----
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:

[source,groovy]
----
parser.withStringFormatValidator("firstUppercase", { str ->
  java.lang.Character.isUpperCase(str.charAt(0))
})

def mySchema = [
  format:"firstUppercase"
]
def schema = parser.parse(mySchema)

----

== Adding custom keywords

For every new keyword type you want to provide, you must implement `link:../../apidocs/io/vertx/ext/json/schema/common/ValidatorFactory.html[ValidatorFactory]`
and provide an instance to `SchemaParser` using `link:../../apidocs/io/vertx/ext/json/schema/SchemaParser.html#withValidatorFactory-io.vertx.ext.json.schema.common.ValidatorFactory-[withValidatorFactory]`.
When parsing happens, the `SchemaParser` calls `link:../../apidocs/io/vertx/ext/json/schema/common/ValidatorFactory.html#canConsumeSchema-io.vertx.core.json.JsonObject-[canConsumeSchema]` for each registered factory.
If the factory can consume the schema, then the method `link:../../apidocs/io/vertx/ext/json/schema/common/ValidatorFactory.html#createValidator-io.vertx.core.json.JsonObject-io.vertx.core.json.pointer.JsonPointer-io.vertx.ext.json.schema.common.SchemaParserInternal-io.vertx.ext.json.schema.common.MutableStateValidator-[createValidator]`
is called. This method returns an instance of `link:../../apidocs/io/vertx/ext/json/schema/common/Validator.html[Validator]`, that represents the object that will perform the validation.
If something goes wrong during `Validator` creation, a `link:../../apidocs/io/vertx/ext/json/schema/SchemaException.html[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 `link:../../apidocs/io/vertx/ext/json/schema/common/SyncValidator.html[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:

[source,groovy]
----
public class PropertiesMultipleOfValidator extends BaseSyncValidator {

  private int multipleOf;

  public PropertiesMultipleOfValidator(int multipleOf) {
    this.multipleOf = multipleOf;
  }

  @Override
  public void validateSync(ValidatorContext context, 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:

[source,groovy]
----
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:

[source,groovy]
----
parser.withValidatorFactory(new examples.PropertiesMultipleOfValidatorFactory())

def mySchema = [
  propertiesMultipleOf:2
]
def schema = parser.parse(mySchema)

----

=== Asynchronous keywords

Synchronous validators must implement the interface `link:../../apidocs/io/vertx/ext/json/schema/common/AsyncValidator.html[AsyncValidator]`.
In this example I add a keyword that retrieves from the Vert.x Event bus an enum of values:

[source,groovy]
----
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(ValidatorContext context, 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:

[source,groovy]
----
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:

[source,groovy]
----
parser.withValidatorFactory(new examples.AsyncEnumValidatorFactory(vertx))

def mySchema = [
  asyncEnum:"enums.myapplication"
]
def 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 `link:../../apidocs/io/vertx/ext/json/schema/draft7/dsl/Schemas.html[Schemas]` and `link:../../apidocs/io/vertx/ext/json/schema/draft7/dsl/Keywords.html[Keywords]`

=== Creating the schema

Inside `link:../../apidocs/io/vertx/ext/json/schema/draft7/dsl/Schemas.html[Schemas]` there are static methods to create the schema:

[source,groovy]
----
Code not translatable
----

=== Using the keywords

For every schema you can add keywords built with `link:../../apidocs/io/vertx/ext/json/schema/draft7/dsl/Keywords.html[Keywords]` methods,
depending on the type of the schema:

[source,groovy]
----
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:

[source,groovy]
----
Code not translatable
----

To create an array schema:

[source,groovy]
----
Code not translatable
----

To create a tuple schema:

[source,groovy]
----
Code not translatable
----

=== `$ref` and aliases

To add a `$ref` schema you can use the `link:../../apidocs/io/vertx/ext/json/schema/common/dsl/Schemas.html#ref-io.vertx.core.json.pointer.JsonPointer-[Schemas.ref]` method.
To assign an `$id` keyword to a schema, use `link:../../apidocs/io/vertx/ext/json/schema/common/dsl/SchemaBuilder.html#id-io.vertx.core.json.pointer.JsonPointer-[id]`

You can also refer to schemas defined with this dsl using aliases. You can use `link:../../apidocs/io/vertx/ext/json/schema/common/dsl/SchemaBuilder.html#alias-java.lang.String-[alias]` to assign an alias to
a schema. Then you can refer to a schema with an alias using `link:../../apidocs/io/vertx/ext/json/schema/common/dsl/Schemas.html#refToAlias-java.lang.String-[Schemas.refToAlias]`:

[source,groovy]
----
Code not translatable
----

=== Using the schema

After you defined the schema, you can call `link:../../apidocs/io/vertx/ext/json/schema/common/dsl/SchemaBuilder.html#build-io.vertx.ext.json.schema.SchemaParser-[build]` to parse and use the schema:

[source,groovy]
----
Code not translatable
----