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.