1. Introduction

The Swagger specification is now better known as OpenAPI Specification. Although Smartbear still supports some of the tools – for instance, Swagger annotations for Java and Kotlin code – wherever possible, it’s best to use OpenAPI versions of everything.

Here, we’ll touch on the advantages of OpenAPI specification in our projects. There are two main approaches: either we have the code and want to generate a specification for it, or we have a specification and now want to generate some code.

In either case, OpenApi/Swagger tools provide an automatic API documentation page.

2. Introducing OpenAPI to a Spring Boot Project

Let us use Gradle to set up our project. We will create a project with Gradle, Kotlin, and JDK 17 on Spring Initializr. Spring Web framework will be our only dependency. Then, we can download a project that is ready to run.

To start displaying documentation in our project, it’s enough to add three dependencies:

implementation("org.springdoc:springdoc-openapi-data-rest:1.6.0")
implementation("org.springdoc:springdoc-openapi-ui:1.6.0")
implementation("org.springdoc:springdoc-openapi-kotlin:1.6.0")

That’s it. Now, we can run the project and access http://localhost:8080/swagger-ui.html. An empty documentation page will appear. Let’s populate it.

3. Generating a Specification From Existing Code

Often, we come to a project that already has some history. Such projects expose their API, but onboarding new clients to that API is usually fraught with difficulties. As usual, everything changes, and the documentation becomes obsolete the day it’s published.

Using our code as the source of truth for our documentation ensures that the documentation always stays relevant. This makes especially good sense if we provide the API to the users outside our organization. Therefore, we can make a new version without consulting anyone. Then we notify our clients when the new set of APIs comes online.

3.1. Using Annotations to Generate a Specification

To achieve the goal of generated documentation, we must annotate our code, both REST controllers and models that serve as arguments and returned values. For the models, we should take a look at the @Schema annotation. It’s the main annotation for data transfer classes:

@Schema(description = "Model for a dealer's view of a car.")
data class DealerCar(
  // Other fields
  @field:Schema(
    description = "A year when this car was made",
    example = "2021",
    type = "int",
    minimum = "1900",
    maximum = "2500"
  )
  val year: Int,
  // More fields
)

This annotation has many attributes: to specify value range, to provide an example value, and to specify a format of a string field, among others. We need to use a Kotlin-specific field target on class fields so that the swagger-core library will process them correctly.

The methods of our REST controller have @Operation annotations explaining their purpose and usage. We can also specify return codes and explain cases in which we return them:

    @Operation(summary = "Sets a price for a chosen car", description = "Returns 202 if successful")
    @ApiResponses(
      value = [
          ApiResponse(responseCode = "202", description = "Successful Operation"),
          ApiResponse(responseCode = "404", description = "Such a car does not exist"),
      ]
    )
    @PostMapping("/price/{carId}", consumes = ["application/json"])
    fun setPrice(
      @PathVariable carId: Int,
      @RequestBody @OASRequestBody(
        description = "New price for the car",
        content = [Content(
          mediaType = "application/json",
          schema = Schema(type = "number", format = "float", minimum = "0.0", example = "23000.34"),
        )]
      ) price: BigDecimal
    ): ResponseEntity<Unit> {
        carService.setPrice(carId, price)
        return ResponseEntity.accepted().build()
    }

Here, we can specify all the responses our method is likely to return and the request body it takes as an argument. Needless to say, this approach may quickly become unmanageable, with the annotations taking much more space than the actual code, so it often pays off to start with a specification.

4. Generating a Server or Client From an Existing Specification

Both complying with a specification on the client and writing an HTTP web-point is often repetitive and tedious. By generating that code, we make our life easier.

Another use of having a specification first is that it’s easier to agree upon a specification between the front-end and back-end.

And, as we already saw, we can achieve more readable sources by keeping the specification and the code apart.

4.2. Using a Specification File to Create a Spring Server

First, we need to create a specification file. An example of such is on the Swagger Online Editor site. However, for a simple demonstration, we’ll use a more straightforward setup which can be found over on our GitHub.

Then, we’ll use the OpenAPI Generator tool. It’s available as a standalone CLI tool and, as such, is supposed to run once to kick the project off. However, when it runs every time the specification changes instead, it provides us with a constant feedback loop on how compliant we are with the specification. Thus, it brings the most advantage.

So, we’ll add it to our project build.gradle.kts:

// The values oasPackage, oasSpecLocation, oasGenOutputDir are defined earlier
tasks.register("generateServer", org.openapitools.generator.gradle.plugin.tasks.GenerateTask::class) {
    input = project.file(oasSpecLocation).path
    outputDir.set(oasGenOutputDir.get().toString())
    modelPackage.set("$oasPackage.model")
    apiPackage.set("$oasPackage.api")
    packageName.set(oasPackage)
    generatorName.set("kotlin-spring")
    configOptions.set(
      mapOf(
        "dateLibrary" to "java8",
        "interfaceOnly" to "true",
        "useTags" to "true"
      )
    )
}

We have to remember to add a dependency on this task into KotlinCompile tasks and add the generated source into the main source set. This plugin can generate the whole REST server, and then we have to supply an ApiDelegate with actual business logic. But for our purposes, just the interfaces are enough:

class CarCrudController(private val carService: CarService) : CarsApi {
    override fun createCar(carBody: CarBody): ResponseEntity<Car> = TODO("Implementation")
    override fun getCar(id: Int): ResponseEntity<Car> = TODO("Implementation")
    override fun getCars(): ResponseEntity<List<Car>> = TODO("Implementation")
}

4.3. Generating a Client From an Existing Specification

Similar to the server generation, we can create a client as well. We just need to change one line in the Gradle script and use generatorName.set(“kotlin”). After that, we should add the necessary dependencies for the client:

implementation("com.squareup.moshi:moshi-kotlin:1.13.0")
implementation("com.squareup.moshi:moshi-adapters:1.13.0")
implementation("com.squareup.okhttp3:okhttp:4.9.3")

Then, we can just call the client:

val car = CarsApi("http://localhost:8080")
  .createCar(ClientCarBody(model = "CM-X", make = "Gokyoro", year = 2021))

5. Conclusion

In this tutorial, we discussed how OpenAPI, the successor of Swagger, can be used to create an API specification if we have source code implementing that API, create the code from the specification, and display user-friendly documentation generated from the same specification.

As usual, the code from the samples can be found over on GitHub.

Comments are closed on this article!