Vert.x OpenAPI

Vert.x OpenAPI extends Vert.x Web to support OpenAPI 3, bringing to you a simple interface to build a Vert.x Web Router conforming your API contract.

Vert.x OpenAPI can:

  • Parse and validate the your OpenAPI 3 contract

  • Generate a router according to your spec, with correct path & methods

  • Provide request parsing and validation based on your contract using Vert.x Web Validation

  • Mount required security handlers

  • Path conversion between OpenAPI style and Vert.x style

  • Route requests to event bus using Vert.x Web API Service

Using Vert.x OpenAPI

To use Vert.x OpenAPI, add the following dependency to the dependencies section of your build descriptor:

  • Maven (in your pom.xml):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-web-openapi</artifactId>
 <version>4.1.5</version>
</dependency>
  • Gradle (in your build.gradle file):

dependencies {
 compile 'io.vertx:vertx-web-openapi:4.1.5'
}

RouterBuilder

RouterBuilder is the main element of this module: It provides the interface to mount request handlers and generates the final Router

To start using Vert.x Web OpenAPI, you must instantiate RouterBuilder with your contract using RouterBuilder.create

For example to load a spec from the local filesystem:

RouterBuilder.create(vertx, "src/main/resources/petstore.yaml")
  .onSuccess(routerBuilder -> {
    // Spec loaded with success
  })
  .onFailure(err -> {
    // Something went wrong during router builder initialization
  });

You can construct a router builder from a remote spec:

RouterBuilder.create(
  vertx,
  "https://raw.githubusercontent" +
    ".com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml")
  .onSuccess(routerBuilder -> {
    // Spec loaded with success
  })
  .onFailure(err -> {
    // Something went wrong during router builder initialization
  });

You can access a private remote spec configuring OpenAPILoaderOptions:

OpenAPILoaderOptions loaderOptions = new OpenAPILoaderOptions()
  .putAuthHeader("Authorization", "Bearer xx.yy.zz");
RouterBuilder.create(
  vertx,
  "https://raw.githubusercontent" +
    ".com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml",
  loaderOptions)
  .onSuccess(routerBuilder -> {
    // Spec loaded with success
  })
  .onFailure(err -> {
    // Something went wrong during router builder initialization
  });

You can modify different behaviours of the router builder with RouterBuilderOptions:

routerBuilder.setOptions(new RouterBuilderOptions());

Access to operations

To access to an Operation defined in contract, use operation. This method returns an Operation instance that you can use to both access the model and assign handlers

To mount an handler to an operation use handler, to mount a failure handler use failureHandler

You can add multiple handlers to same operation, without overwrite the existing ones.

For example:

routerBuilder
  .operation("awesomeOperation")
  .handler(routingContext -> {
    RequestParameters params =
      routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
    RequestParameter body = params.body();
    JsonObject jsonBody = body.getJsonObject();
    // Do something with body
  }).failureHandler(routingContext -> {
  // Handle failure
});
Important

You can’t access to contract operations without operationId. The operations without operationId are ignored by the RouterBuilder

Vert.x OpenAPI mounts the correct ValidationHandler for you, so you can access to request parameters and request body. Refer to Vert.x Web Validation documentation to learn how to get request parameters & request body and how to manage validation failures

Configuring `AuthenticationHandler`s defined in the OpenAPI document

Security is a important aspect of any API. OpenAPI defines how security is expected to be enforced in the api document.

All security scheme information resided under the /components/securitySchemes component. The information in this object is different and specific for each type of authentication. To avoid double configuration, this module allows you to provide factories for authentication handlers that will receive the source configuration from the source document.

For example, given an document that defines Basic Authentication as follows:

openapi: 3.0.0
...
components:
 securitySchemes:
   basicAuth:     # <-- arbitrary name for the security scheme
     type: http
     scheme: basic

This can be configured with a factory as:

routerBuilder
  .securityHandler("basicAuth")
  .bindBlocking(config -> BasicAuthHandler.create(authProvider));

While this example is quite simple to configure, creating an authentication handler that requires the configuration such as the API Key handler can extract the config:

 openapi: 3.0.0
 ...
 # 1) Define the key name and location
 components:
   securitySchemes:
     ApiKeyAuth:        # arbitrary name for the security scheme
       type: apiKey
       in: header       # can be "header", "query" or "cookie"
       name: X-API-KEY  # name of the header, query parameter or cookie
routerBuilder
  .securityHandler("ApiKeyAuth")
  .bindBlocking(config ->
    APIKeyHandler.create(authProvider)
      .header(config.getString("name")));

Or you can configure more complex scenarios such as OpenId Connect which require server discovery.

openapi: 3.0.0
...
# 1) Define the security scheme type and attributes
components:
 securitySchemes:
   openId:   # <--- Arbitrary name for the security scheme. Used to refer to it from elsewhere.
     type: openIdConnect
     openIdConnectUrl: https://example.com/.well-known/openid-configuration
routerBuilder
  .securityHandler("openId")
  .bind(config ->
    OpenIDConnectAuth
      .discover(vertx, new OAuth2Options()
        .setClientId("client-id") // user provided
        .setClientSecret("client-secret") // user provided
        .setSite(config.getString("openIdConnectUrl")))
      .compose(authProvider -> {
        AuthenticationHandler handler =
          OAuth2AuthHandler.create(vertx, authProvider);
        return Future.succeededFuture(handler);
      }))
  .onSuccess(self -> {
    // Creation completed with success
  })
  .onFailure(err -> {
    // Something went wrong
  });

The API is designed to be fluent so it can be used in a short notation, for example:

routerBuilder
  .securityHandler("api_key")
  .bindBlocking(config -> APIKeyHandler.create(authProvider))
  .operation("listPetsSingleSecurity")
  .handler(routingContext -> {
    routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage("Cats and Dogs")
      .end();
  });

// non-blocking bind
routerBuilder
  .securityHandler("oauth")
  .bind(config -> OpenIDConnectAuth.discover(vertx, new OAuth2Options(config))
    .compose(oidc -> Future.succeededFuture(
      OAuth2AuthHandler.create(vertx, oidc))))

  .onSuccess(self -> {
    self
      .operation("listPetsSingleSecurity")
      .handler(routingContext -> {
        routingContext
          .response()
          .setStatusCode(200)
          .setStatusMessage("Cats and Dogs")
          .end();
      });
  });

Blocking vs NonBlocking

From the examples above it is noticeable that handlers can be added in a blocking or not blocking way. The reason for non blocking way usage is not just to support handlers like OAuth2. The non-blocking way can be useful for handlers like JWT or basic authentication where the authentication provider requires loading of keys or configuration files.

Here is an example with JWT:

routerBuilder
  .securityHandler("oauth")
  .bind(config ->
    // as we don't want to block while reading the
    // public key, we use the non blocking bind
    vertx.fileSystem()
      .readFile("public.key")
      // we map the future to a authentication provider
      .map(key ->
        JWTAuth.create(vertx, new JWTAuthOptions()
          .addPubSecKey(new PubSecKeyOptions()
            .setAlgorithm("RS256")
            .setBuffer(key))))
      // and map again to create the final handler
      .map(JWTAuthHandler::create))

  .onSuccess(self ->
    self
      .operation("listPetsSingleSecurity")
      .handler(routingContext -> {
        routingContext
          .response()
          .setStatusCode(200)
          .setStatusMessage("Cats and Dogs")
          .end();
      }));

Map AuthenticationHandler to OpenAPI security schemes

You have seen how you can map an AuthenticationHandler to a security schema defined in the contract. The previous examples are validating and will fail your route builder if the configuration is missing.

There could be cases where the contract is incomplete and you explicitly want to define security handlers. In this case the API is slightly different and will not enforce any contract validation. Yet, the security handlers will be available to the builder regardless.

For example, given your contract has a security schema named security_scheme_name:

routerBuilder.securityHandler(
  "security_scheme_name",
  authenticationHandler);

You can mount AuthenticationHandler included in Vert.x Web, for example:

routerBuilder.securityHandler("jwt_auth",
  JWTAuthHandler.create(jwtAuthProvider));

When you generate the Router the router builder will solve the security schemes required for an operation. It fails if there is a missing AuthenticationHandler required by a configured operation.

For debugging/testing purpose you can disable this check with setRequireSecurityHandlers

Not Implemented Error

Router builder automatically mounts a default handler for operations without a specified handler. This default handler fails the routing context with 405 Method Not Allowed/501 Not Implemented error. You can enable/disable it with setMountNotImplementedHandler and you can customize this error handling with errorHandler

Response Content Type Handler

Router builder automatically mounts a ResponseContentTypeHandler handler when contract requires it. You can disable this feature with setMountResponseContentTypeHandler

Operation model

If you need to access to your operation model while handling the request, you can configure the router builder to push it inside the RoutingContext with setOperationModelKey:

options.setOperationModelKey("operationModel");
routerBuilder.setOptions(options);

// Add an handler that uses the operation model
routerBuilder
  .operation("listPets")
  .handler(
    routingContext -> {
      JsonObject operation = routingContext.get("operationModel");

      routingContext
        .response()
        .setStatusCode(200)
        .setStatusMessage("OK")
        // Write the response with operation id "listPets"
        .end(operation.getString("operationId"));
    });

Body Handler

Router builder automatically mounts a BodyHandler to manage request bodies. You can configure the instance of BodyHandler (e.g. to change upload directory) with bodyHandler.

multipart/form-data validation

The validation handler separates file uploads and form attributes as explained:

  • If the parameter doesn’t have an encoding associated field:

    • If the parameter has type: string and format: base64 or format: binary is a file upload with content-type application/octet-stream

    • Otherwise is a form attribute

  • If the parameter has the encoding associated field is a file upload

The form attributes are parsed, converted in json and validated, while for file uploads the validation handler just checks the existence and the content type.

Custom global handlers

If you need to mount handlers that must be executed for each operation in your router before the operation specific handlers, you can use rootHandler

Router builder handlers mount order

Handlers are loaded by the router builder in this order:

  1. Body handler

  2. Custom global handlers

  3. Configured `AuthenticationHandler`s

  4. Generated ValidationHandler

  5. User handlers or "Not implemented" handler (if enabled)

Generate the router

When you are ready, generate the router and use it:

Router router = routerBuilder.createRouter();

HttpServer server =
  vertx.createHttpServer(new HttpServerOptions().setPort(8080).setHost(
    "localhost"));
server.requestHandler(router).listen();

This method can fail with a RouterBuilderException.

Tip

If you need to mount all the router generated by router builder under the same parent path, you can use mountSubRouter:

Router global = Router.router(vertx);

Router generated = routerBuilder.createRouter();
global.mountSubRouter("/v1", generated);