Mastodon Icon GitHub Icon LinkedIn Icon RSS Icon

How to document a Kotlin/Spring application with Springdoc and OpenAI

Here we go again with a new article derived from my work notes. As you already know, I am rewriting a backend application in Kotlin and — in the process — I am improving all the horrors of legacy code I can find. In this article, we will look at one critical aspect of software development (especially for REST applications): the documentation.

Why Automatic Documentation is Important

Before this change, we documented the REST interface of this server application in Postman. Postman is a nice application, it is nice to use ad produces a really nice-looking documentation. Unfortunately, it has a big problem: it requires manual intervention.

As you know, developers are lazy scum. If you rely on developers to update your Postman collection after every change, you are a fool. Nobody really does that when you need it, and I am the worst of all.1

You want the documentation job to be as much as connected to the development job. For this reason, I opted for an OpenAPI package that generate automatically my sweet REST documentation from code.

Yes, by using these kinds of services, your code will be slightly bloated by a bunch of annotations and extra code just for documentation but, in my opinion, it is a price that I am delighted to pay!

Basic Setup

Installing the Spring OpenAPI Package

To implement this automagical generator we just need to add the following dependencies in Maven:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-data-rest</artifactId>
  <version>1.4.3</version>
</dependency>
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-ui</artifactId>
  <version>1.4.3</version>
</dependency>

Or, if you are using Gradle:

1
2
implementation("org.springdoc:springdoc-openapi-data-rest:1.4.3")
implementation("org.springdoc:springdoc-openapi-ui:1.4.3")

Now, after you have compiled and ran the application, you can just go to localhost/swagger-ui.html and enjoy a basic but complete REST documentation for your server application.

Springdoc, in fact, will parse your controllers and will generate the OpenAPI specification of your API. All that without a single line of code.

An example of the documentation generated by Springdoc.
Figure 1. An example of the documentation generated by Springdoc.

Improve Kotlin Support

If your application is in Kotlin, you may want to add a dependency. This will improve the introspection of Springdoc capability while parsing Kotlin code.

On Maven:

1
2
3
4
5
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-kotlin</artifactId>
  <version>1.4.3</version>
</dependency>

Or on Gradle:

1
implementation("org.springdoc:springdoc-openapi-kotlin:1.4.3")

Basic Annotations

The end result is already great: you have a nice list of endpoint and, if you click on them, you may see that Springdoc could infer URL parameters, input and output types. That’s not bad for something that required zero effort from our side.

However, we may do better. We would like to add descriptions, explanations, and improve the schemas of the different input and outputs. Let’s see some annotation that can help us.

The first one is the @Schema annotation. This annotation is useful to describe the different part of our Data Transfer Object. Let’s assume that we have an AddressDTO class. We may want to describe the different fields of the class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class AddressDto {
    @Schema(description = "First line of the address.", example = "Strada Cala Garibaldi")
    var line1: @NotNull(groups = [OnCreate::class, OnUpdate::class]) @Size(min = 1, max = 120, groups = [OnCreate::class, OnUpdate::class]) String? = null
    @Schema(description = "Second line of the address.", example = "Something")
    var line2: @Size(min = 1, max = 120, groups = [OnCreate::class, OnUpdate::class]) String? = null
    @Schema(description = "The recipient city.", example = "Caprera")
    var city: @Size(min = 1, max = 80, groups = [OnCreate::class, OnUpdate::class]) String? = null
    @Schema(description = "The recipient state.", example = "Sardinia")
    var state: @Size(min = 1, max = 80, groups = [OnCreate::class, OnUpdate::class]) String? = null
    @Schema(description = "The recipient postal code", example = "07024")
    var postalCode: @Size(min = 1, max = 30, groups = [OnCreate::class, OnUpdate::class]) String? = null
    @Schema(description = "The recipient country.", example = "Italy")
    var country: @Size(min = 1, max = 80, groups = [OnCreate::class, OnUpdate::class]) String? = null
} 

As you can see, I use the @Schema annotation to add a description and an example value to the class attributes. The result is exactly what you expect:

An example of schema for AddressDto.
Figure 2. An example of schema for AddressDto.

Another two essential annotations are the @Operator and @ApiResponses ones. These are used to document the controllers, a.k.a., the actual endpoints. As an example, let’s see a GET /users endpoint that returns the list of all users.

The @Operator endpoint is used like in the following example:

1
@Operation(summary="Get a list of Users", description = "Returns a list of Users.")

You can use it to provide a small summary and a small description the use case for that specific endpoint and REST verb.

The @ApiResponses annotation, instead may be used in this way:

1
2
3
4
@ApiResponses(value = [
    ApiResponse(responseCode="200", description = "Successful Operation",
            content = [Content(mediaType = "application/json", schema = Schema(implementation = PagedUsersResponse::class))])
])

The annotation contains an array of @ApiResponse annotations. Each one of them is one of the possible outcomes of a request to the endpoint (e.g., a successful operation, but also the possible error codes returned by the endpoint).

Other than the responseCode and description of the response, the @ApiResponse annotation allows you to specify the content of the response’s answer. The example, I think, it is self-explanatory. The important annotation here is the @Schema annotation. This is different from the previous one: this is used to reference an existing class as the response’s schema. In my example, PagedUsersResponse.

This part is where 90% of your documentation effort will be. You need to document every single endpoint. However, the end result is worth the effort.

Produce a better documentation

With that, I think you have everything you need to maintain a timeless documentation masterpiece. However, if you are like me and you thrive on useless details, we can easily transform such documentation from usefull to top-quality level. How? You guessed right. With powerful Annotation Magic.

Add OpenAPI/SwaggerUI Global Configuration

You can easily configure the overall aspect of your documentation with a simple Spring Bean. Create a new Kotlin class and paste the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class OpenApiConfig {

    @Bean
    fun customOpenAPI(): OpenAPI {
        return OpenAPI()
                .components(Components())
                .info(Info()
                        .title("My Fantastic API")
                        .description("This is the API documentation for my fantastic awesome incredible software."))
    }
}

This will add a nice title and a nice description to the main documentation page. There are more elements you can configure. Unfortunately, I have not found a good documentation for this. I’ll update the article if I find something interesting.

Enable Token Authentication

The Swagger UI used to visualize your OpenAPI documentation can also be used to test your API. Exactly. The page includes a way to send your server the example content to the endpoint and visualize the response.

To enable that, however, you need to configure the UI to allow user’s authorization. Even this time, the solution is just one Annotation Magic trick away.

1
2
3
4
5
6
7
@Configuration
@SecurityScheme(
    name = "bearerAuth",
    type = SecuritySchemeType.HTTP,
    bearerFormat = "JWT",
    scheme = "bearer"
) 

In my application, I am using a Bearer Token authorization scheme. To enable that in my documentation I just need to copy the above code on top of the OpenApiConfig bean I defined before. That’s it. Not the UI will show a nice Authorize button.

By clicking on it, you will open a modal dialog to enter the bearer token and authorize your test calls.

The authorization modal window.
Figure 4. The authorization modal window.

Other Tips

Here they are some more interesting tips. I will add more tips as I find new obstacles in my documentation work.

Add support for custom Generic Containers

In my code I use a custom generic container for pagination defined as:

1
2
3
4
5
6
open class PageDto<T> {
    var items: List<T>? = null
    var page: Int? = null
    var pageSize: Int? = null
    var total: Long? = null
} 

So, for example, if I return a page of User instances, I will define my response body as PageDto<UserDto>. Unfortunately, in the @ApiResponse annotation I cannot reference such generic class. If I try to do something like this:

1
Schema(implementation = PageDto<UserDto>::class))

The application will not compile because we cannot get a ::class reference of a generic definition.

The good news is that there is an easy workaround. In your controller you need to define an internal private class that extends the generic class you need. Like this one:

1
2
// This is a tweak to export the right format in the OpenAPI documentation.
private class PagedUsersResponse : PageDto<UserDto>()  

And then use this class in your @Schema annotation.

1
Schema(implementation = PagedUsersResponse::class))

Problem solved. It is annoying to have to define a class just for your documentation engine. However, it is a sacrifice I am happy to do.

Conclusion

That’s how I am documenting my Spring application. What is your opinion? Do you like to make your code a bit dirty to have an auto-generated documentation or do you are willing to die on the hill of “code is just for logic”? Let me know! See you next time.


  1. That’s funny given that I find writing documentation almost fun and I am constantly bitching about how “we really should write documentation!” ↩︎

comments powered by Disqus