1. OpenAPI
This module helps automating the generation of API documentation using Jooby projects. jooby-openapi works by examining an application at build time to infer API semantics based on byte code and optional annotations.
Automatically generates documentation in JSON/YAML format APIs. This documentation can be completed by javadoc comments or using swagger-api annotations.
This library supports:
-
OpenAPI 3 (json and yaml)
-
Swagger UI (Optional)
-
Redoc (Optional)
-
AsciiDoc Output (Optional)
Checkout the [demo project](https://github.com/jooby-project/library-demo)
1.1. Configuration
<properties>
<application.class>myapp.App</application.class>
</properties>
...
<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>4.0.13</version>
<executions>
<execution>
<goals>
<goal>openapi</goal>
</goals>
<configuration>
<specVersion>...</specVersion>
<adoc>
<file>...</file>
</adoc>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
|
The default phase of the plugin execution is |
1.2. Usage
To learn how it works, let’s write a simple Pet API:
{
install(new OpenAPIModule()); (1)
path("/pets", () { (2)
get("/", ctx -> {
PetRepository repo = ...;
return repo.list();
});
});
}
| 1 | Install OpenAPIModule |
| 2 | Write your API |
The OpenAPIModule read from classpath the generated json and yaml files. To generate
them you need to build your project.
mvn clean package
./gradlew build
You will find the files in the output build directory. If your application is bar.Foo, then:
target/classes/bar/Foo.json target/classes/bar/Foo.yaml
build/classes/java/main/bar/Foo.json build/classes/java/main/bar/Foo.yaml
|
This is the main difference with previous version. We moved from runtime to build time generation. This way we:
|
The OpenAPI generator works exactly the same for MVC routes (a.k.a Controller):
{
install(new OpenAPIModule());
mvc(new Pets_());
}
/**
* Pet Library.
*/
@Path("/pets")
public class Pets {
/**
* List pets.
*
* @return All pets.
*/
@GET
public List<Pet> list() {
...
}
}
The Maven plugin and Gradle task provide two filter properties includes and excludes. These
properties filter routes by their path pattern. The filter is a regular expression.
1.3. Documenting your API
Full/complete example available [here](https://github.com/jooby-project/library-demo)
1.3.1. JavaDoc comments
JavaDoc comments are supported on Java in script and MVC routes.
/**
* My Library.
* @version 5.4.1
* @tag Books. All about books.
* @server.url https://books.api.com
* @server.description Production
* @x-logo https://my.api.com/logo.png
*/
public class App extends Jooby {
{
install(new OpenAPIModule());
/*
* Books operations.
* @tag Books
*/
path("/api/books", () -> {
/*
* Query and filter books.
*
* @param query Book filter.
* @return List of matching books.
* @operationId query
*/
get("/", ctx -> {
var query = ctx.query(BookQuery.class);
});
/*
* Find a book by ISBN.
* @param isbn ISBN code.
* @return A book.
* @throws BookNotFoundException When a book doesn't exist. <code>404</code>
* @operationId bookByIsbn
*/
get("/{isbn}", ctx -> {
var query = ctx.query(BookQuery.class);
});
});
}
}
/**
* Book query.
*
* @type Book type.
* @aga Book age.
*/
public record BookQuery(BookType type, int age) {}
/**
* Books can be broadly categorized into fiction and non-fiction
*/
public enum BookType {
/**
* Fiction includes genres like fantasy, science fiction, romance, and mystery.
*/
Fiction,
/**
* Non-fiction encompasses genres like history, biography, and self-help.
*/
NonFiction
}
A JavaDoc comment is split into:
-
summary: Everything before the first
.or<p>paragraph -
description: Everything after the first
.or<p>paragraph
Whitespaces (including new lines) are ignored. To introduce a new line, you must use a <p>.
1.3.2. Supported OpenAPI tags
| Tag | Main | Controller | Method | Description |
|---|---|---|---|---|
@version 4.0.4 |
[x] |
|||
@contact.name |
[x] |
|||
@contact.url |
[x] |
|||
@contact.email |
[x] |
|||
@license.name |
[x] |
|||
@license.url |
[x] |
|||
@securityScheme.name |
[x] |
|||
@securityScheme.in |
[x] |
|||
@securityScheme.paramName |
[x] |
|||
@securityScheme.flows.implicit.authorizationUrl |
[x] |
|||
@securityScheme.flows.implicit.scopes.name |
[x] |
|||
@securityScheme.flows.implicit.scopes.description |
[x] |
|||
@securityRequirement |
[x] |
[x] |
Name of the |
|
@server.description |
[x] |
|||
@x- |
[x] |
[x] |
[x] |
Tag starting with |
@tag.name |
[x] |
[x] |
[x] |
Tag name |
@tag.description |
[x] |
[x] |
[x] |
Tag Description |
@tag |
[x] |
[x] |
[x] |
Shortcut for previous |
@operationId <name> |
[x] |
Defined the operationId, useful for script routes. |
||
@param <name> |
[x] |
Operation parameter |
||
@return |
[x] |
Operation default response |
||
@throws |
[x] |
Additional non-success responses types. The HTTP response code is taken from |
This feature is only available for Java routes. Kotlin source code is not supported.
1.3.3. Documentation Template
The OpenAPI output generates some default values for info and server section. It generates
the necessary to follow the specification and produces a valid output. These sections can be override
with better information/metadata.
To do so just write an openapi.yaml file inside the conf directory to use it as template.
openapi: 3.0.1
info:
title: My Super API
description: |
Nunc commodo ipsum vitae dignissim congue. Quisque convallis malesuada tortor, non
lacinia quam malesuada id. Curabitur nisi mi, lobortis non tempus vel, vestibulum et neque.
...
version: "1.0"
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
paths:
/api/pets:
get:
operationId: listPets
description: List and sort pets.
parameters:
name: page
descripton: Page number.
All sections from template file are merged into the final output.
The extension property: x-merge-policy controls how merge must be done:
-
ignore: Silently ignore a path or operation present in template but not found in generated output. This is the default value.
-
keep: Add a path or operation to final output. Must be valid path or operation.
-
fail: Throw an error when path or operation is present in template but not found in generated output.
The extension property can be added at root/global level, paths, pathItem, operation or parameter level.
|
Keep in mind that any section found here in the template overrides existing metadata. |
1.4. Swagger Annotations
Optionally this plugin depends on some OpenAPI annotations. To use them, you need to add a dependency to your project:
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations</artifactId>
<version>2.2.41</version>
</dependency>
Once you added to your project, you can annotate your routes:
import io.swagger.v3.oas.annotations.Operation;
...
public class App extends Jooby {
{
path("/pets", () -> {
get("/{id}", this::findPetById)
});
}
@Operation(
summary = "Find a pet by ID",
description = "Find a pet by ID or throws a 404"
)
public Pet findPetById(Context ctx) {
PetRepo repo = require(PetRepo.class);
long id = ctx.path("id").longValue();
return repo.find(id);
}
}
The OpenAPI annotations complement the openAPI byte code parser by adding documentation or being more specific about an operation, parameter, response type, response status, etc.
Annotations works as documentation but also as a way to override what was generated by the byte code parser.
Annotations are supported at script routes (using the technique described before) and mvc routes.
If you look at the example, there is no documentation for path parameter: id, still this parameter
is going to be present in the OpenAPI files (present, but without documentation).
To add documentation just do:
@Operation( summary = "Find a pet by ID", description = "Find a pet by ID or throws a 404", parameters = @Parameter(description = "Pet ID") )
If the parameter annotation doesn’t specify a name, parameter binding follows a positional assignment.
1.4.1. OpenAPIDefinition
This annotation is supported at the application level:
@OpenAPIDefinition(
info = @Info(
title = "Title",
description = "description",
termsOfService = "Terms",
contact = @Contact(
name = "Jooby",
url = "https://jooby.io",
email = "support@jooby.io"
),
license = @License(
name = "Apache",
url = "https://jooby.io/LICENSE"
),
version = "10"
),
tags = @Tag(name = "mytag")
)
class App extends Jooby {
{
// All routes now have the default tag: `Pets`
}
}
1.4.2. Tags
Tagging is supported at three different levels:
@Tag(name = "Pets", description = "Pet operations")
class App extends Jooby {
{
// All routes now have the default tag: `Pets`
}
}
@Tag(name = "Pets", description = "Pet operations")
@Path("/pets")
class Pets {
// All web method now have the default tag: `Pets`
}
@Tag(name = "Pets", description = "Pet operations")
public List<Pet> list(Context ctx) {
...
}
For multiple tags use the @Tags annotation or the tags property of @OpenAPIDefinition
and/or @Operation annotations.
1.4.3. Responses & Status
The default response code is Success(200) (or NO_CONTENT(204) for DELETE mvc routes). Now, if
you need to:
-
document the default response
-
use a custom response code
-
use multiple response codes
You need the ApiResponse annotation:
@ApiResponse(description = "This is the default response")
@ApiResponse(responseCode = "201", description = "This is the default response")
@ApiResponses({
@ApiResponse(description = "This is the default response"),
@ApiResponse(responseCode = "500"),
@ApiResponse(responseCode = "400"),
@ApiResponse(responseCode = "404")
})
1.5. Output/Display
1.5.1. Ascii Doc
Setup
1) create your template: doc/library.adoc
= 📚 {{info.title}} Guide
:source-highlighter: highlightjs
{{ info.description }}
== Base URL
All requests start with: `{{ server(0).url }}/library`
=== Summary
{{ routes | table(grid="rows") }}
2) add to build process
...
<plugins>
...
<plugin>
<groupId>io.jooby</groupId>
<artifactId>jooby-maven-plugin</artifactId>
<version>4.0.13</version>
<executions>
<execution>
<goals>
<goal>openapi</goal>
</goals>
<configuration>
<adoc>
<file>doc/library.adoc</file>
</adoc>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
3) The output directory will have two files: - library.adoc (final asciidoctor file) - library.html (asciidoctor output)
1. Overview
The Jooby OpenAPI Template Engine is a tool designed to generate comprehensive AsciiDoc (.adoc) documentation directly from your Jooby application’s OpenAPI model.
It uses pebble as a pre-processor to automate redundant tasks. Instead of manually writing repetitive documentation for every endpoint, you write a single template that pulls live data from your code (routes, schemas, javadoc).
Pebble Syntax Primer
You mix standard AsciiDoc text with Pebble logic.
-
{{ expression }}: Output. Use this to print values to the file.Example:
{{ info.title }}prints the API title. -
{% tag %}: Logic. Use this for control flow (loops, variables, if/else) without printing output.Example:
{% for route in routes %} … {% endfor %}loops through your API routes.
2. The Pipeline Concept
Data generation follows a flexible pipeline architecture. You start with a source and can optionally transform it before rendering.
{{ Source | [Mutator] | Display }}
-
Source: Finds an object in the OpenAPI model (e.g., a route or schema).
-
Mutator (Optional): Transforms or filters the data (e.g., extracting just the body, or filtering parameters).
-
Display: Renders the final output (e.g., as JSON, a Table, or a cURL command).
Examples
-
Simple: Source → Display
{{ info.description }} -
Chained: Source → Mutator → Display
{{ GET("/users") | request | body | example | json }}
3. Data Sources (Lookups)
These functions are your entry points to locate objects within the OpenAPI definition.
| Function | Description | Example |
|---|---|---|
|
Generic lookup for an API operation. |
|
|
Shorthand for |
|
|
Shorthand for |
|
|
Shorthand for respective HTTP methods. |
|
|
Looks up a Schema/Model definition by name. |
|
|
Selects a specific Tag group (containing name, description, and routes). |
|
|
Returns a collection of all available routes in the API. |
|
|
Selects a server definition from the OpenAPI spec by index. |
|
|
Generates an error response object. |
|
|
Generates status code descriptions. Accepts: |
|
4. Mutators (Transformers)
Mutators modify the data stream. They are optional but powerful for drilling down into specific parts of an object.
| Filter | Description | Input Context |
|---|---|---|
|
Extracts the Request component from an operation. |
Operation |
|
Extracts a specific Response component. |
Operation |
|
Extracts the Body payload definition. |
Operation / Request / Response |
|
Extracts form-data parameters specifically. |
Operation / Request |
|
Extracts parameters. |
Operation / Request |
|
Populates a Schema with example data. |
Schema / Body |
|
Takes a complex Schema and returns a new Schema containing only direct fields. Removes nested objects and deep relationships. |
Schema / Body |
5. Display (Renderers)
The final step in the chain. These filters determine how the data is written to the AsciiDoc file.
| Filter | Description |
|---|---|
|
Generates a ready-to-use cURL command. |
|
Renders the raw HTTP Request/Response wire format. |
|
Renders the full relative URI. |
|
Renders the input object as a formatted JSON block. |
|
Renders the input object as a YAML block. |
|
Renders a standard AsciiDoc/Markdown table. |
|
Renders a simple bulleted list. |
|
Renders an ascii doc on schema. |
6. Common Recipes
A. Documenting a Route (The "Standard" Block)
Use this pattern to document a specific endpoint, separating path parameters from query parameters.
// 1. Define the route
{% set route = GET("/library/books/{isbn}") %}
=== {{ route.summary }}
{{ route.description }}
// 2. Render Path Params
.Path Parameters
{{ route | parameters(path) | table }}
// 3. Render Query Params
.Query Parameters
{{ route | parameters(query) | table }}
// 4. Render Response + example
.Response
{{ route | response | body | example | json }}
// 5. Render Response and Override Status Code
.Created(201) Response
{{ route | response(201) | http }}
// 6. Render Response 400
.Bad Request Response
{{ route | response(400) | http }}
{{ route | response(400) | json }}
B. The "Try It" Button (cURL)
Provide a copy-paste command for developers.
{{ POST("/items") | curl }}
{{ POST("/items") | curl("-i", "-H", "'Accept: application/xml'") }}
{{ POST("/items") | curl(language="bash") }}
C. Simulating specific scenarios
Use the path filter to inject specific values into the URL, making the documentation more realistic.
// Scenario: Searching for Sci-Fi books on page 2
GET {{ GET("/search") | path(q="Sci-Fi", page=2) }}
Output: GET /search?q=Sci-Fi&page=2
D. Simplifying Complex Objects
If your database entities have deep nesting (e.g., Book → Author → Address → Country), use truncate to show a summary view.
// Full Graph (Huge JSON)
{{ schema("Book") | example | json }}
// Summary (Flat JSON)
{{ schema("Book") | truncate | example | json }}
E. Error Response Reference
You can generate error examples using the standard format or a custom structure.
1. Default Structure
Generates a JSON with statusCode, reason, and message.
.404 Not Found
{{ error(404) | json }}
2. Custom Error Structure
Define a variable named error with a map containing your fields. Use {{code}}, {{message}}, and {{reason}} placeholders which the engine will automatically populate.
// Define the custom error shape once
{%- set error = {
"code": "{{code}}",
"message": "{{message}}",
"reason": "{{reason}}",
"timestamp": "2025-01-01T12:00:00Z",
"support": "help@example.com"
} -%}
// Now generate the error. It will use the map above.
.400 Bad Request
{{ error(400) | json }}
Output:
{
"code": 400,
"message": "Bad Request",
"reason": "Bad Request",
"timestamp": "2025-01-01T12:00:00Z",
"support": "help@example.com"
}
F. Dynamic Tag Loop
Automatically document your API by iterating over tags defined in Java.
{% for tag in tags %}
== {{ tag.name }}
{% for route in tag.routes %}
=== {{ route.summary }}
{{ route.method }} {{ route.path }}
{% endfor %}
{% endfor %}
7. Advanced Patterns
G. Reusable Macros (DRY)
As your template grows, use macros to create reusable UI components (like warning blocks or deprecated notices) to keep the main logic clean.
{# 1. Define the Macro at the top of your file #}
{% macro deprecationWarning(since) %}
[WARNING]
====
This endpoint is deprecated since version {{ since }}.
Please use the newer version instead.
====
{% endmacro %}
{# 2. Use it inside your route loop #}
{% if route.deprecated %}
{{ deprecationWarning("v2.1") }}
{% endif %}
H. Security & Permissions
If your API uses authentication (OAuth2, API Keys), the security property on the route contains the requirements.
{% if route.security %}
.Required Permissions
[cols="1,3"]
|===
|Type | Scopes
{# Iterate through security schemes #}
{% for scheme in route.security %}
{% for req in scheme %}
| *{{ loop.key }}* | {{ req | join(", ") }}
{% endfor %}
{% endfor %}
|===
{% endif %}
I. Linking to Schema Definitions
AsciiDoc supports internal anchors. You can automatically link a route’s return type to its full Schema definition elsewhere in the document.
{# 1. Create Anchors in your Schema Loop #}
{% for s in schemas %}
[id="{{ s.name }}"]
== {{ s.name }}
{{ s | table }}
{% endfor %}
{# 2. Link to them in your Route Loop #}
.Response Type
Returns a {{ route | response | link }} object.
1.5.2. Swagger UI
To use swagger-ui just add the dependency to your project:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-swagger-ui</artifactId>
<version>4.0.13</version>
</dependency>
The swagger-ui application will be available at /swagger. To modify the default path, just call swaggerUI(String)
1.5.3. Redoc
To use redoc just add the dependency to your project:
<dependency>
<groupId>io.jooby</groupId>
<artifactId>jooby-redoc</artifactId>
<version>4.0.13</version>
</dependency>
The redoc application will be available at /redoc. To modify the default path, just call redoc(String)