Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

API documentation: OpenAPI and Swagger UI

A frontend developer or another team needs to know which addresses the API has, what they accept and what they return. Such a description is usually written in the OpenAPI format — a JSON file with the list of routes, parameters and data models. From it, Swagger UI draws a page where each request can be viewed and run right away. Javalin has an official plugin, javalin-openapi: you describe handlers with the @OpenApi annotation, and at compile time an annotation processor (a program that runs together with the compiler) builds the specification from them.

Dependencies

The plugin's version number follows Javalin: for Javalin 7.2.3 the plugin 7.2.3 fits. Add a property and two dependencies to pom.xml:

<!-- pom.xml -->
<properties>
    <!-- ... -->
    <javalin.openapi.version>7.2.3</javalin.openapi.version>
</properties>
 
<dependency>
    <groupId>io.javalin.community.openapi</groupId>
    <artifactId>javalin-openapi-plugin</artifactId>
    <version>${javalin.openapi.version}</version>
</dependency>
<dependency>
    <groupId>io.javalin.community.openapi</groupId>
    <artifactId>javalin-swagger-plugin</artifactId>
    <version>${javalin.openapi.version}</version>
</dependency>

And add the annotation processor itself to the maven-compiler-plugin settings:

<!-- pom.xml -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.14.1</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>io.javalin.community.openapi</groupId>
                <artifactId>openapi-annotation-processor</artifactId>
                <version>${javalin.openapi.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Describing the routes

The annotation goes on a controller method. All annotation classes live in the io.javalin.openapi package:

// src/main/java/com/example/blog/PostController.java
@OpenApi(
        path = "/posts/{id}",
        methods = HttpMethod.GET,
        summary = "Get a post by id",
        tags = "Posts",
        pathParams = @OpenApiParam(name = "id", type = Long.class, required = true),
        responses = {
                @OpenApiResponse(status = "200", content = @OpenApiContent(from = Post.class)),
                @OpenApiResponse(status = "404", description = "Post not found")
        }
)
public void getOne(Context ctx) {
    // code unchanged
}
 
@OpenApi(
        path = "/posts",
        methods = HttpMethod.POST,
        summary = "Create a post",
        tags = "Posts",
        security = @OpenApiSecurity(name = "BasicAuth"),
        requestBody = @OpenApiRequestBody(content = @OpenApiContent(from = NewPost.class), required = true),
        responses = {
                @OpenApiResponse(status = "201", content = @OpenApiContent(from = Post.class)),
                @OpenApiResponse(status = "400", description = "Validation failed"),
                @OpenApiResponse(status = "401", description = "Login required")
        }
)
public void create(Context ctx) {
    // code unchanged
}

path and methods must match the real route: the plugin does not compare them with apiBuilder. from = Post.class turns the record into a JSON schema; for a list you write from = Post[].class. Describe getAll and delete the same way.

Registering the plugins

// src/main/java/com/example/blog/Main.java
config.registerPlugin(new OpenApiPlugin(openapi -> {
    openapi.withDefinitionConfiguration((version, builder) -> {
        builder.info(info -> info.title("Blog API").version("1.0.0"));
        builder.withBasicAuth("BasicAuth");
    });
}));
config.registerPlugin(new SwaggerPlugin());

OpenApiPlugin (package io.javalin.openapi.plugin) serves the specification at /openapi, and SwaggerPlugin (io.javalin.openapi.plugin.swagger) serves the /swagger page. withBasicAuth("BasicAuth") declares the sign-in scheme that @OpenApiSecurity refers to. The documentation addresses have no roles, so the Auth::check filter lets them through.

Trying it out

Run mvn compile exec:java and request the specification:

curl http://localhost:8080/openapi
{
  "openapi" : "3.1.0",
  "info" : {
    "title" : "Blog API",
    "version" : "1.0.0"
  },
  ...
    "/posts/{id}" : {
      "get" : {
        "tags" : [ "Posts" ],
        "summary" : "Get a post by id",
        "parameters" : [ {
          "name" : "id",
          "in" : "path",
          "required" : true,
          "schema" : {
            "type" : "integer",
            "format" : "int64"
          }
        } ],
  ...

Open http://localhost:8080/swagger in the browser: the "Posts" group shows four requests, and the NewPost and Post schemas are at the bottom. The Authorize button takes a login and password, and Try it out sends a real request to the server.

Common mistake

Without the annotationProcessorPaths block the project compiles without errors, but the specification is empty:

{
  "info" : {
    "title" : "Blog API",
    "version" : "1.0.0"
  },
  "components" : {
    "securitySchemes" : { ... },
    "schemas" : { }
  },
  "paths" : { }
}

There is not even an openapi field with the version number, and instead of the list of requests Swagger UI says Unable to render this definition: The provided definition does not specify a valid version field. The annotation processor writes the file target/classes/openapi-plugin/openapi-default.json — if it is missing, check the maven-compiler-plugin settings and rebuild the project with mvn clean compile.

Thanks for reading!

Donate to the site — we work hard for you.