Overview

This plugin allows you to generate OpenAPI / Swagger specifications (currently 3.0 compatible) for your Spincast REST API, as JSON or YAML.

It automatically uses information from your routes and provides tools for you to complete/tweak the specifications to expose as the documentation for your API.

Introduction

OpenAPI is one of the most popular ways of documenting a REST API. It is a specification around which many tools and libraries have been created. The most known of those tools are probably the Swagger Editor and the Swagger UI.

The OpenAPI ecosystem may be a little bit confusing at first because there are a lot of different ways of using the specification. Some libraries will provide a way of generating application code (controllers, validators, etc.) given an existing specifications file... This is called the "top-down" approach (or "API first" approach). Others libraries will do the opposite: they will generate the specifications file from your application code... This is called the "bottom-up" approach (or "code first" approach) and is the one used by this plugin.

Whatever approach you use, the most important part of an OpenAPI implementation is to be able to provide the specifications to the consumers of your API, as JSON or as YAML. You could send those specifications by email to developers in charge of developing a client using your API (for example as a "specs.yaml" file), but most of the time you expose the specifications as an HTTP endpoint so they can be easily accessed!

How this plugin works

Some frameworks try to develop an OpenAPI implementation using a custom parser and custom code to generate the final specifications. This is not a trivial task since the OpenAPI specification contains many details! It can be easy to forget something or to generate something invalid. Also, keeping up-to-date with new versions of the specification may be challenging...

This is why we decided to develop this plugin in a way that code from the official reference implementation can be reused as much as possible. By doing so, we make sure we use battle tested and well-maintained code. We also make sure it is super easy to upgrade to a new version of the specification in the future.

The problem with the official Java implementation is that it is made specifically for JAX-RS based applications and Spincast is not! Spincast is indeed more flexible: it allows you to define your routes as you want (inline if required), without any annotations. Also, routes can be dynamically added to the Router, they don't necessarily have to be defined in controllers using special annotations.

So, how can we reuse the official code?

This plugin automagically generate JAX-RS classes from the Spincast code. It does this using Byte Buddy. With those generated dummy classes, it is possible to reuse the code from the official swagger-jaxrs2 library, without any change. In other words, the job of this plugin is only to generate pseudo JAX-RS classes... Everything else is done by the official library: the parsing and the specifications generation.

To help you specify the information to include in the resulting specifications, this plugin also provides a way to reuse another official part of OpenAPI/Swagger: the Swagger annotations. As you'll see in the Documenting the routes section, the most important annotation you can use is @Operation.

If you don't provide any explicit information about your routes, using annotations or using plain YAML, the plugin will still be able to generate basic specifications. You manually add the extra pieces of information required for the final specifications to be completed.

Usage

Accessing the generated specifications

First of all, the specifications resulting from collecting the various pieces of information about your API are accessible as an OpenAPI Java object. You access this object via the SpincastOpenApiManager#getOpenApi() method. It is this object that will be converted to generate the final specifications as JSON or as YAML!

You generate the specifications:

As you can see, the SpincastOpenApiManager component is at the very core of this plugin. You can inject it anywhere you need to interact with it.

Serving the specifications as an HTTP endpoint

When your application is properly documented (we'll learn how to do this in the next section), it is often desirable to serve the resulting specifications as an HTTP endpoint so they can be accessed easily.

You create such endpoint as any other one. If your specifications won't change at runtime, using a Dynamic Resource is the way to go! Let's see how you can do this:

@Inject
protected SpincastOpenApiManager spincastOpenApiManager;

// As JSON
router.file("/specifications.json")
      .pathRelative("/generated/specifications.json")
      .handle((context) -> {
    context.response().sendJson(spincastOpenApiManager.getOpenApiAsJson());
});

// As YAML
router.file("/specifications.yaml")
      .pathRelative("/generated/specifications.yaml")
      .handle((context) -> {
    context.response().sendCharacters(spincastOpenApiManager.getOpenApiAsYaml(), "text/yaml");
});

Documenting the routes

If you don't add any explicit information, the generated specifications will still be available but will be basic. They will contain information about your routes: their HTTP methods, their paths, their consumes content-types and their path parameters.

To add more information, you can use the .specs(...) method available when you define the routes. You pass to this method the extra information using the official annotations. Read the official documentation to learn more about those annotations.

Here's a step-by-step guide on how to add the annotation(s)... You first start by using the .specs(...) method:

router.POST("/books/${bookId:<AN+>}")
      .specs(...)
      .handle(booksController::get);

In that method, you create an anonymous class based on the SpecsObject interface. This acts as a container for the annotations so they can later be retrieved by Spincast:

router.POST("/books/${bookId:<AN+>}")
      .specs(new SpecsObject() {})
      .handle(booksController::get);

You annotate this anonymous class with @Specs:

router.POST("/books/${bookId:<AN+>}")
      .specs(@Specs(...) new SpecsObject() {})
      .handle(booksController::get);

And you can finally add a @Operation, @Consumes, and/or @Produces annotations inside @Specs:

router.POST("/books/${bookId:<AN+>}")
      .specs(new @Specs(consumes = @Consumes({"application/xml"}),
                        produces = @Produces({"application/json"}),
                        value = @Operation(summary = "My summary",
                                           description = "My description"
                        )) SpecsObject() {})
      .handle(booksController::get);

This may seem a little bit complicated at first but this is what allows the reuse of the official, well supported, Swagger reference implementation code!

Here's another example:

router.POST("/users")
      .specs(new @Specs(@Operation(
             parameters = {
                 @Parameter(name = "full",
                            in = ParameterIn.QUERY,
                            schema = @Schema(description = "Return all informations?")),
           })) SpecsObject() {})
      .handle(booksController::get);

Explanation :

  • 2 : We use the .specs(...) method to start adding specifications to the route. The @Specs annotation is only a wrapper allowing us to use the official @Operation annotation.
  • 3-6 : The extra OpenAPI specifications to associate with the route. In this simple example, we only document a "full" querystring parameter.
  • 7 : The annotations need to be added on something for Spincast to be able to retrieve them. You create an anonymous class from the SpecsObject interface to store them.

Using YAML instead of annotations

If you prefer that to annotations, you can also document a route using plain YAML:

router.POST("/books/${bookId:<AN+>}")
      .specs("post:\n" +
             "  operationId: getBook\n" +
             "  responses:\n" +
             "    default:\n" +
             "      description: default response\n" +
             "      content:\n" +
             "        application/json: {}\n")
      .handle(booksController::get);

The YAML specifications you provide must start by the HTTP methods (you can provide more than one!). And you must follow the syntax required by the OpenAPI specification.

Note that the indentation in your YAML content IS important and an exception will be thrown if it is invalid.

Ignoring some routes

You may want to prevent some of your routes to be added to the final specifications. To do so, you can call specsIgnore() on those routes:

router.POST("/books/${bookId:<AN+>}")
      .specsIgnore()
      .handle(booksController::get);

If you need to ignore routes that have been added by a third-party plugin, you can do so by using their id (if they have one), or their HTTP method and path otherwise:

@Inject
protected SpincastOpenApiManager spincastOpenApiManager;

// Ignoring a route by its id:
spincastOpenApiManager.ignoreRoutesByIds("someRouteId");

// Ignoring a route by its HTTP method and path:
spincastOpenApiManager.ignoreRouteUsingHttpMethodAndPath(HttpMethod.POST, "/some-path");

Global OpenAPI information

Using setOpenApiBase(...) on the SpincastOpenApiManager component, you can provide any OpenAPI information you want to be part of the generated specifications. You do this by creating a custom OpenAPI object:

OpenAPI openApiBase = new OpenAPI();
Info info = new Info().title("The name of your REST API")
                      .description("A description of your API...")
                      .termsOfService("https://example.com/tos");
openApiBase.info(info);

getSpincastOpenApiManager().setOpenApiBase(openApiBase);

Note that you can use that base OpenAPI object to fully define the specifications of your API, without using any information from the routes themselves! You do so by disabling the automatic specifications via the plugin configurations, and then specify everything (paths included) using this base OpenAPI object.

Dynamic specifications

If your specifications may change at runtime (for example if a new route may be added at some point), there are some things to know:

  • You shouldn't use a Dynamic Resource route to serve the specifications since the first generated version would be cached forever. You need to use a standard route.
  • The code adding new information to the specifications (or simply adding a new route) needs to call clearCache(). This is required since Spincast caches the specifications for performance reasons.
  • If, at some point, you want to reset everything in the SpincastOpenApiManager instance (the cache, the ignored routes, the base OpenAPI object, etc.), you can call resetAll().

Configurations

To tweak the configurations used by this plugin, you need to bind a custom implementation of the SpincastOpenApiBottomUpPluginConfig interface. You can also extend the default SpincastOpenApiBottomUpPluginConfigDefault implementation as a base class.

The available configurations are:

  • boolean isDisableAutoSpecs()

    If this method returns true no automatic specifications are going to be created. You are then responsible to add all the information about your API using the base OpenAPI object.

    The default is to return false.

  • String[] getDefaultConsumesContentTypes()

    The default content-types that are going to be used as the @Consumes information of a route if not explicitly defined otherwise.

    The default is application/json.

    Note that specifying a @Consumes annotation inside .specs(...) will overwrite that default, but also using .accept(...) on the route definition!

  • String[] getDefaultProducesContentTypes()

    The default content-types that are going to be used as the @Produces information of a route if not explicitly defined otherwise.

    The default is application/json.

Installation

1. Add this Maven artifact to your project:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-openapi-bottomup</artifactId>
    <version>2.2.0</version>
</dependency>

2. Add an instance of the SpincastOpenApiBottomUpPlugin plugin to your Spincast Bootstrapper:


Spincast.configure()
        .plugin(new SpincastOpenApiBottomUpPlugin())
        // ...