Web Architecture, Java Ecosystem, Software Craftsmanship

RESTful API Documentation with Swagger and AsciiDoc

Posted on Feb 19, 2017

Writing documentation manually for a RESTful API can be laborious. On the other hand, relying exclusively on the generation of the documentation (e.g. with swagger-ui) is often not sufficient. There are always aspects (like common use cases and workflows) that need to be described manually. Let’s see how we can combine these two approaches with the help of AsciiDoc!

Generating HTML Documentation for a RESTFul API utilizing Swagger and AsciiDoc

Big Picture

The workflow for creating a documentation for a REST API.

The workflow for creating a documentation for a REST API.

  1. The Java resource classes (containing the JAX-RS and Swagger annotations) are parsed and a Swagger specification is generated.
  2. Next, AsciiDoc files are generated based on the Swagger specification.
  3. We add manually written AsciiDoc files.
  4. Finally, all AsciiDoc files are put together and a nice HTML or PDF document is created.
The final HTML documentation of the REST API. The Section '3. Usage' is manually written and Part '4. Paths' is generated based on our JAX-RS Resources.

The final HTML documentation of the REST API. The Section '3. Usage' is manually written and Part '4. Paths' is generated based on our JAX-RS Resources.

Advantages

  • We can mix generated and manually written documentation at the same time and merge it into one document.
    • The information that can be inferred from the code are generated (resource paths, HTTP methods, parameters, payload format etc.).
    • Additionally, we can add chapters in the documentation that are manually written. Within these chapters, we are free to write whatever we want. For instance, this is useful when you want to demonstrate concrete workflows and describe general remarks about the API.
  • We use the nice markup language AsciiDoc to write the manual parts of the documentation (instead of a dedicated Wiki or Word document). Developers love markup languages.
  • The documentation is located within the project repository. So it’s under version control, which makes it easy to track changes and use other VCS-related features like branches and pull requests.
  • This approach helps to keep the documentation up to date. Reasons:
    • The generation bases on the code. It is automated and executed during the build. You can’t forget it.
    • The manually written documentation is located next to the project source code. Therefore, it’s easy for every developer to find and change it.
    • Ideally, the service itself provides the documentation via an HTTP endpoint. So you don’t have to manually copy the documentation to a dedicated file server. Just deploy your service and the updated documentation with it.
  • The Swagger specification can also be exposed by the service (/swagger.json or /swagger.yaml). This way, clients can use them to generate clients.

Drawbacks

  • It takes some effort to set up the described toolchain. This may only be justified in the case of public APIs.
  • You bloat your JAX-RS resource classes and DTO classes with a flood of Swagger annotations. In the worse case, a resource class contains more JAX-RS and Swagger annotations then code.
  • If you use different DTOs for creation and retrieval (which is a good idea) you have to duplicate many annotations.

Implementation

I’ve implemented this approach in an example project rest-api-doc-jaxrs-swagger-asciidoc on GitHub.

The implementation of the workflow.

The implementation of the workflow.

The main advantage of this implementation is that the whole generation chain is exclusively performed during the build. At runtime, the service only provides the created static files.

Let’s take a look the most important implementation details.

The folder /src/docs/asciidoc contains the manually written AsciiDoc files.

The Java class SwaggerAndAsciiDocGenerator parses the JAX-RS resources and generates both the Swagger spec and the AsciiDoc files. The class is called during the Maven build. It uses the library Swagger2Markup to create AsciiDoc files out of the Swagger specification.

/**
 * a) parses the resource classes and generates the swagger spec,
 * b) takes the swagger spec and generates the asciidoc.
 * <br>
 * Arguments:
 * arg[0]: target folder for swagger.json and swagger.yaml. e.g. "target/classes"
 * arg[1]: target folder for the generated asciidoc. e.g. "target/asciidoc/generated"
 */
public class SwaggerAndAsciiDocGenerator {

    public static void main(String[] args) throws IOException {
        Path swaggerTargetFolder = Paths.get(args[0]);
        Path asciiDocTargetFolder = Paths.get(args[1]);

        System.out.println("Creating Swagger Spec in " + swaggerTargetFolder);
        createSwagger(swaggerTargetFolder);
        System.out.println("Generating AsciiDoc in " + asciiDocTargetFolder);
        convertSwaggerToAsciiDoc(swaggerTargetFolder, asciiDocTargetFolder);
        System.out.println("Done.");
    }

    private static void createSwagger(Path swaggerTargetFolder) throws IOException {
        BeanConfig config = new BeanConfig();
        config.setVersion("v1");
        config.setTitle("Band API");
        config.setDescription("An API to retrieve and create bands.");
        config.setSchemes(new String[]{"http"});
        config.setHost("localhost:8080");
        config.setBasePath("");
        config.setResourcePackage("de.philipphauer.blog.resources");
        config.setScan();//this "setter" triggers the scanning... nice naming...
        Swagger swagger = config.getSwagger();

        String json = Json.pretty().writeValueAsString(swagger);
        Path jsonFile = swaggerTargetFolder.resolve("swagger.json");
        Files.write(jsonFile, json.getBytes(StandardCharsets.UTF_8));

        String yaml = Yaml.mapper().writeValueAsString(swagger);
        Path yamlFile = swaggerTargetFolder.resolve("swagger.yaml");
        Files.write(yamlFile, yaml.getBytes(StandardCharsets.UTF_8));
    }

    private static void convertSwaggerToAsciiDoc(Path swaggerTargetFolder, Path asciiDocTargetFolder){
        Path swaggerFile = swaggerTargetFolder.resolve("swagger.json");
        Swagger2MarkupConverter.from(swaggerFile)
                .build()
                .toFolder(asciiDocTargetFolder);
    }
}

The pom.xml contains the invocation of the SwaggerAndAsciiDocGenerator (via the exec-maven-plugin) and the asciidoctor-maven-plugin.

<properties>
        <asciidoctor.input.directory>${project.basedir}/src/docs/asciidoc</asciidoctor.input.directory>
        <generated.asciidoc.directory>${project.build.directory}/asciidoc/generated</generated.asciidoc.directory>
        <asciidoctor.html.output.directory>${project.build.outputDirectory}</asciidoctor.html.output.directory> <!-- target/classes -->
</properties>

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.5.0</version>
    <configuration>
        <mainClass>de.philipphauer.blog.apiDocGen.SwaggerAndAsciiDocGenerator</mainClass>
        <arguments>
            <argument>${project.build.outputDirectory}</argument> 
            <argument>${generated.asciidoc.directory}</argument> 
        </arguments>
    </configuration>
    <executions>
        <execution>
            <id>generate-swagger-and-asciidoc</id>
            <phase>process-classes</phase>
            <goals>
                <goal>java</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.asciidoctor</groupId>
    <artifactId>asciidoctor-maven-plugin</artifactId>
    <version>1.5.3</version>
    <configuration>
        <sourceDirectory>${asciidoctor.input.directory}</sourceDirectory>
        <sourceDocumentName>index.adoc</sourceDocumentName>
        <attributes>
            <icons>font</icons>
            <doctype>book</doctype>
            <toc>left</toc>
            <toclevels>3</toclevels>
            <numbered></numbered>
            <hardbreaks></hardbreaks>
            <sectlinks></sectlinks>
            <sectanchors></sectanchors>
            <generated>${generated.asciidoc.directory}</generated>
            <stylesheet>custom.css</stylesheet>
        </attributes>
    </configuration>
    <executions>
        <execution>
            <id>output-html</id>
            <phase>test</phase>
            <goals>
                <goal>process-asciidoc</goal>
            </goals>
            <configuration>
                <backend>html5</backend>
                <sourceHighlighter>highlightjs</sourceHighlighter>
                <outputDirectory>${asciidoctor.html.output.directory}</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Finally, the created HTML file and the Swagger file are served by the DocumentationResource.

@Path("/")
public class DocumentationResource {

    @GET
    @Path("swagger.json")
    @Produces(MediaType.APPLICATION_JSON)
    public String swaggerJson() {
        return getFileContent("swagger.json");
    }

    @GET
    @Path("swagger.yaml")
    @Produces("application/yaml")
    public String swaggerYaml(){
        return getFileContent("swagger.yaml");
    }

    @GET
    @Path("application-doc.html")
    @Produces(MediaType.TEXT_HTML)
    public String doc(){
        return getFileContent("index.html");
    }

    private String getFileContent(String fileName) {
        try {
            URL url = Resources.getResource(fileName);
            return Resources.toString(url, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw Throwables.propagate(e);
        }
    }
}

Build and start the service

mvn package
java -jar target/rest-api-doc-jaxrs-swagger-asciidoc-1.0-SNAPSHOT.jar server config.yml

The service will now provide the following resources:

# API:
GET http://localhost:8080/bands
GET http://localhost:8080/bands/<uuid>
POST http://localhost:8080/bands

# Swagger Specification:
GET http://localhost:8080/swagger.json
GET http://localhost:8080/swagger.yaml

# API HTML Documentation:
GET http://localhost:8080/application-doc.html

Hint: In order to call the swagger.yaml resource from another domain you have to set a proper CORS header. See CORSFilter. This way, the Swagger spec can be consumed by a Swagger-UI (which can be deployed separately).

Tip: Shorten Turn-Around Time

Executing the whole Maven lifecycle just to regenerate the documentation is clumsy. To speed up the authoring process you can use the following commands (see generate-documentation.sh).

# Case 1: After changing a JAX-RS resource java class
# Precondition: Compile the class with our IDE (Ctrl+Shift+F9 in IntelliJ IDEA). Then:
mvn exec:java@generate-swagger-and-asciidoc asciidoctor:process-asciidoc@output-html

# Case 2: After changing a manually written AsciiDoc file (in src/docs/asciidoc)
mvn asciidoctor:process-asciidoc@output-html

# Open target/classes/index.html (no need to start the service)

Variations and Alternatives

  • Are you using Spring MVC annotations instead of JAX-RS? Check out Springfox. It generates a Swagger specification based on Spring MVC annotations.
  • Some approaches generate the AsciiDoc within a test (see Springfox example or Swagger2Markup example). They start the whole service (or at least parts of the Spring context), retrieve the Swagger spec from the HTTP endpoint and pass it to Swagger2Markup. I’m skeptical about this approach. First, using a test for AsciiDoc generation feels hacky for me. Second, it only makes sense if your Swagger spec is only created at runtime by your service. However, I prefer to create the Swagger spec during the Maven build and serve this static file when the Swagger endpoint is called. This setup is simpler and more intuitive. Besides, there is no runtime overhead.
  • Why do I don’t use the existing maven-plugin for generating the Swagger spec based on JAX-RS annotations? Yes, there is kongchen’s swagger-maven-plugin, but in the past the plugin didn’t always use the latest version of the Swagger JAX-RS integration. This caused some troubles for me. That’s why I decided to call the JAX-RS integration programmatically in the SwaggerAndAsciiDocGenerator during the build. This gives me full control and reduces dependencies.
  • The JAX-RS Integration also provides the class ApiListeningResource. It provides an HTTP endpoint to retrieve the Swagger spec (like /swagger.json). However, it scans your JAX-RS resources every time the endpoint it called. Moreover, why should I do this at runtime at all? The Swagger spec is static. All information required are available during the build. But the source code of ApiListeningResource and SwaggerSerializers are good examples to see the usage of the Swagger JAX-RS API.
  • Note, that there is the nice swagger2markup-maven-plugin. It allows creating the AsciiDoc files based on a Swagger spec during the build. However, I don’t use it (for now), because I create the Swagger Spec in the SwaggerAndAsciiDocGenerator anyway. So it’s the simplest solution to generate the AsciiDoc files in the same class as well right after the Swagger generation.
  • Finally, I recommend taking a look at the helpful spring-swagger2markup-demo.
  • If you like to create a PDF document, check out this example.
  • The JAXRS-Analyzer is an alternative to the Swagger’s JAX-RS integration. Instead of retrieving the information via reflection, it uses bytecode analysis. This way, it can retrieve information from the method body (like the returned status code or HTTP headers).