Generate Kotlin client for a complex web API
Tags: kotlin, openapi, tutorial, teamcity
Published: 2023-02-09 17:36:01

Automation for many routines starts with interaction via API. This case can be treated in many ways, but I want to take a look at interacting with web API using a generated Kotlin client.
I found a lot of academic examples for generating Kotlin clients (most of them are based on the [PetStore](https://petstore.swagger.io/)), but nothing was close to real-world examples.
My goal is to build a Kotlin client for a complex API and see how it works.
[Here is the source code](https://github.com/mariakrol/open-api-playground) of the Kotlin project that I will use in the post.

## Domain <a name="domain"></a>
I chose the TeamCity API to play with. It is huge and complex, and also contains models with names that clash with built-in Java classes.

## What is the goal? <a name="goal"></a>
* A client has generated using the Kotlin language.
* The build task of the project depends on the generation of the client.

## Preparation <a name="preparation"></a>
### Install TeamCity <a name="install-teamcity"></a>
All the examples have shown in this post are performed on a TeamCity installed on my machine.
The installation process is fairly straightforward, and specific settings for the TeamCity installation process are beyond the scope of this article.

### Take a look at the API <a name="observe-api"></a>
On the official TeamCity website we can find a lot of documentation about the API. In addition, we can see the automatically generated sources and find a path to the endpoint where the API schema is stored in Swagger format (`/app/rest/swagger.json`).
In the list of generated models, we can find types that have names that conflict with Java's built-in.

| TeamCity API Type | Java Type              | 
| ----------------- |:----------------------:|
| [File](https://www.jetbrains.com/help/teamcity/rest/file.html)              | java.io.File           |
| [Type](https://www.jetbrains.com/help/teamcity/rest/type.html)              | java.lang.reflect.Type |

Failed to create a client for Java. The generator imports Java's types instead of TeamCity's.
There are bugs described for the Java client in both the [Swagger generator](https://github.com/swagger-api/swagger-codegen/issues/8134) and the [OpenAPI generator](https://github.com/OpenAPITools/openapi-generator/issues/5083). Let's see how the generator behaves when building a Kotlin client.

## Generate Kotlin client <a name="generate"></a>
### Generate the client with the default settings of the generator <a name="generate-by-default"></a>

[The source code](https://github.com/mariakrol/open-api-playground/tree/generate-client-with-jvm-okhttp4--SUCCESS)

#### Create a project <a name="create-project"></a>
Let’s start with the creation of a project. We have used the basic template of the project with Kotlin language and Gradle as a build system.

![Settings of the new project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x23o4kihw8psl2ntlk0a.png)

We need to add the API Schema file to the project's resources:

![API schema in the resources](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hyqz0o8ulpmxxhuczjcp.png)

#### Choose a Generator <a name="choose-generator"></a>
We have two different generators to choose from:
* [Swagger Codegen](https://github.com/swagger-api/swagger-codegen)
* [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator)

The main difference is described [on the official page](https://openapi-generator.tech/docs/faq/#what-is-the-difference-between-swagger-codegen-and-openapi-generator) of the OpenAPI generator:

> _What is the difference between Swagger Codegen and OpenAPI Generator?_
Swagger Codegen is driven by SmartBear while OpenAPI Generator is driven by the community. More than 40 top contributors and template creators of Swagger Codegen have joined OpenAPI Generator as the founding team members. For more details, see the Fork Q&A.
Swagger is a trademark owned by SmartBear and the use of the term "Swagger" in this project is for demo (reference) purposes only.

Both generators do not work with the TeamCity API out of the box. For the OpenAPI generator, all problems are infrastructure related: The generator produces invalid Kotlin code with some combinations of libraries, and some other combinations produce valid code, but the environment (HTTP clients or serializers) cannot be fine-tuned. The Swagger generator produces invalid Kotlin code not related to libraries or settings: for example, variable names were generated with dashes.
So let's go with the OpenAPI generator.

#### Generation process <a name="start-generation"></a>
We need to add the OpenAPI plugin to Gradle. ([plugin page in MavenCentral](https://search.maven.org/artifact/org.openapitools/openapi-generator))

__build.gradle.kts__
```kotlin
plugins {
   kotlin("jvm") version "1.8.0"
   id("org.openapi.generator") version "6.3.0"
}
```
Now we need to set up the task of the generator:
* select the language;
* pass path to file with API specification;
* pass path where to put generated sources;
* select names of packages with generated code.

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set("$buildDir/generated")
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
}
```
We need to run the target task in Gradle to check the result:

![OpenAPI generator task in Gradle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9o2y29zs64qmqkj4at2j.png)

The process fails, we get the error `Out of memory. Java heap space`:

![Java heap out of memory](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hibl1t0d9trav0n8sb40.png)

Gradle uses a [default value of 512 Mb available](https://docs.gradle.org/current/userguide/build_environment.html#sec:configuring_jvm_memory) for the Java heap. This is not enough for the OpenAPI generator in IDEA to build the client. We can change the size of the Java heap as well as other Gradle parameters in the `gradle.properties` file.
I use a very simple selection algorithm:
Every time I fail with the error, I increase the size of the heap to twice the size. 4Gb is not enough. You can play with the heap size between 4Gb and 8Gb to select a smaller value if needed.

__gradle.properties__
```
org.gradle.jvmargs=-Xmx8192m
```
Now the process is finished successfully!

![Successfully generated](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u5r8zp6v7f3x3h14cdi0.png)

#### Build the project <a name="build-project"></a>
We need to add the generated files to the list of source paths in order for them to be included in the compilation:

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set(generatedSourcesPath)
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
}

kotlin.sourceSets["main"].kotlin.srcDir("$generatedSourcesPath/src/main/kotlin")
```
Now we need to add a dependency of the Kotlin compilation task on the generation of the client. The compilation process must be done after the generation:

__build.gradle.kts__
```kotlin
tasks.withType<KotlinCompile>().configureEach {
   dependsOn("openApiGenerate")
}
```
Let's run the build process and observe the errors (required dependencies are not included). Go through the errors and add the dependencies:
* Unresolved reference: okhttp3 → [add from mvnrepository](https://mvnrepository.com/artifact/com.squareup.okhttp3)
* Unresolved reference: com.squareup.moshi → [add from mvnrepository](https://mvnrepository.com/artifact/com.squareup.moshi/moshi-kotlin) (everything needed to build a generated client is in this package)

__build.gradle.kts__
```kotlin
dependencies {
   testImplementation(kotlin("test"))

   // OpenAPI client
   implementation("com.squareup.okhttp3:okhttp:4.10.0")
   implementation("com.squareup.moshi:moshi-kotlin:1.14.0")
}
```
The second pass of the build is successful.

#### Calling the API from the client <a name="call-api"></a>
Calling the API methods from the client is required to verify that the client works at runtime. I have chosen two endpoints to call: User and Project.
To make it easier, let's add a small wrapper that will create a basic HTTP client and set it up initially:

__BaseApiClient.kt__
```kotlin
open class BaseApiClient {
   companion object {
       const val host = ConfigurationProvider.teamCityHost
       val baseClient: OkHttpClient = getApiClient()

       private fun getApiClient(): OkHttpClient {
           return OkHttpClient.Builder()
               .build()
       }
   }
}
```
The TeamCity Web API is protected by authorization. The OkHttpClient builder does not know anything about authorization. OkHttpClient can be extended with `interceptor`, so we need to add our custom to pass authentication data.
The correct way of authorization is a separate complex topic and it is beyond the scope of this post. I have implemented the simplest, but not the most appropriate way: TeamCity provides the ability to generate authorization tokens with long expiration, I created a token and use it to authorize my requests:

![TeamCity authorization tokens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ulhw9lj3ycrznys5k9g3.png)

This static token is passed to the interceptor without refreshing and so on:

__AuthorizationInterceptor.kt__
```kotlin
class AuthorizationInterceptor : Interceptor {
   override fun intercept(chain: Interceptor.Chain): Response {
       val newRequest = chain.request().signedRequest()
       return chain.proceed(newRequest)
   }

   private fun Request.signedRequest(): Request {
       return this.newBuilder()
           .header("Authorization", "Bearer ${ConfigurationProvider.token}")
           .build()
   }
}
```
Pass it to the client:

__BaseApiClient.kt__
```kotlin
private fun getApiClient(authorizationInterceptor: AuthorizationInterceptor): OkHttpClient {
   return OkHttpClient.Builder()
       .addInterceptor(authorizationInterceptor)
       .build()
}
```
Now we can wrap any endpoint. Let's start with the UserAPI and create methods to create, get, and delete a user:

__UserApiWrapper.kt__
```kotlin
class UserApiWrapper : BaseApiClient() {
   private val userApi: UserApi = UserApi(host, baseClient)

   fun createUser(): User {
       val newUser = User(
           username = "userName".appendRandomNumericPostfix(),
           name = "name".appendRandomNumericPostfix(),
           email = "${("fakeMail".appendRandomNumericPostfix())}@example.com",
           password = "pass".appendRandomNumericPostfix()
       )

       return userApi.addUser(body = newUser)
   }
  
   fun getUser(userName: String) : User {
       return userApi.getUser(userName)
   }

   fun deleteUser(user: User) {
       userApi.deleteUser(user.username!!)
   }
}
```
Call the API from a dummy test:

![Successful run of dummy test](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/843kfms59rfsjrh4r0e0.png)

The user is successfully created. Let's continue with a TeamCity project:

__ProjectsApiWrapper.kt__
```kotlin
class ProjectsApiWrapper : BaseApiClient() {
   private val projectApi: ProjectApi = ProjectApi(host, baseClient)

   fun createProject(): Project {
       val newProject = NewProjectDescription(name = "simple_tc_project".appendRandomNumericPostfix())

       return projectApi.addProject(newProject)
   }

   fun getProject(projectName: String): Project {
       return projectApi.getProject(projectName)
   }
}
```
The execution fails with the error `Platform class java.io.File requires explicit JsonAdapter to be registered`:

![Error: JsonAdapter is not registered](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5sk29byd4iizz2717zf3.png)
This happens because the Moshi library is used for serialization, and it cannot handle collections without explicitly registered adapters. This bug has already been [reported in the repo](https://github.com/OpenAPITools/openapi-generator/issues/8560).
Since we do not create instances of Moshi that do the serialization (the generator does it under the hood), we cannot register adapters.

#### Fix the generated client by changing settings <a name="change-settings"></a>
We are faced with the problem described above when using a client generated with default settings. Now we need to check the settings of the generator to use something else for serialization.
The OpenAPI generator has a [lot of different settings](https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/kotlin.md). But most interesting for us now is
* __library__ - This setting is used to control a whole set of libraries used to generate a client.
* __serializationLibrary__ - This setting controls only one serialization library. 

So, we can go in two different ways: 
* use the setting `serializationLibrary` to replace the Moshi library with something else and continue to use OkHttp for HTTP client
* use the setting `library` and replace the whole group of libraries

#### Use setting ‘serializationLibrary’ to replace the Moshi library <a name="change-serialization-lib"></a>
First I try to replace the serialization library. The setting ‘serializationLibrary’ has 3 different values:
* [moshi](https://github.com/square/moshi)
* [gson](https://github.com/google/gson)
* [jackson](https://github.com/FasterXML/jackson)

The first one is not suitable for the current case, so let’s play with the two remaining.

##### Use `gson` as serialization library <a name="change-serialization-gson"></a>
To use the gson library, we need to add the dependency for it and change the settings of the task of the OpenAPI generator:

__build.gradle.kts__
```kotlin
implementation("com.google.code.gson:gson:2.10.1")
```

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set(generatedSourcesPath)
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
   configOptions.set(mapOf("serializationLibrary" to "gson"))
}
```
The process of building this configuration goes successfully, we can start testing and see the green result!

##### Use `jackson` as a serialization library <a name="change-serialization-jackson"></a>
Now I try to do the same for the `jackson` library. Here are the dependency and the settings of the generator:

__build.gradle.kts__
```kotlin
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2")
```

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set(generatedSourcesPath)
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
   configOptions.set(mapOf("serializationLibrary" to "jackson"))
}
```
The build and test processes have also been successfully completed!

#### Use setting `library` to replace the whole set of libraries required for the generation process <a name="change-libs"></a>

The `library` setting has many possible values. Each value describes the platform, library for HTTP client, and library for serialization:

| Setting | Platform      | HTTP Client  | JSON processor |
|-------|:---------------:|:-------------|----------------:|
| jvm-ktor | Java Virtual Machine | Ktor | Gson, Jackson  |
| jvm-okhttp4 | Java Virtual Machine | OkHttp | Moshi |
| jvm-okhttp3 | Java Virtual Machine | OkHttp | Moshi |
| jvm-retrofit2 | Java Virtual Machine | Retrofit | Moshi |
| multiplatform | Kotlin multiplatform | Ktor | Kotlinx Serialization |
| jvm-volley | JVM for Android | Volley | gson |
| jvm-vertx | Java Virtual Machine | Vert.x Web Client | Moshi, Gson or Jackson |

The setting  `jvm-okhttp4` which is used by default was covered by the previous paragraph. Since the platform, I use for the playground is JVM, we cannot try to use jvm-volley. 
The remaining cases:
* jvm-ktor
* multiplatform
* jvm-vertx

##### Using `jvm-ktor` group to generate: invalid Kotlin code <a name="change-libs-jvm-ktor"></a>

[The source code](https://github.com/mariakrol/open-api-playground/tree/generate-client-with-jvm-ktor--FAIL)

I start with `jvm-ktor`. The official documentation says that it uses `jackson` for serialization.

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set(generatedSourcesPath)
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
   configOptions.set(mapOf("library" to "jvm-ktor"))
}
```
Build the project. We can find errors regarding dependencies (we did not add a required set), and invalid Kotlin code: classes that describe endpoints of the API pass non-existent parameters to the superclass:

![Unexpected parameter](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4rsub198le2pg9me92un.png)
So, that case is useless for Kotlin clients now.

##### Using the `multiplatform` group to generate: native Kotlin variant <a name="change-libs-multiplatform"></a>

[The source code](https://github.com/mariakrol/open-api-playground/tree/generate-client-with-multiplatform--SUCCESS)

Now let’s try to use the `multiplatform` group of libraries:

__build.gradle.kts__
```kotlin
val generatedSourcesPath = "$buildDir/generated"
val apiDescriptionFile = "$rootDir/src/main/resources/teamCityRestApi-v2018.1-swagger2.0.json"
val apiRootName = "com.makrol.teamcity.api.client"

openApiGenerate {
   generatorName.set("kotlin")
   inputSpec.set(apiDescriptionFile)
   outputDir.set(generatedSourcesPath)
   apiPackage.set("$apiRootName.api")
   invokerPackage.set("$apiRootName.invoker")
   modelPackage.set("$apiRootName.model")
   configOptions.set(mapOf("library" to "multiplatform"))
}
```
In case we build the project, we would see only errors related to missing dependencies and not Moshi in this list. Looks good. So let's add the dependencies. ([io.ktor group in mvnrepository](https://mvnrepository.com/artifact/io.ktor)):

__build.gradle.kts__
```kotlin
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
```
Now we need to update the infrastructure. We have the interceptor for `OkHttp` authorization. `Ktor` allows us to use several different engines (as well as `OkHttp`), but for authorization purposes it is better to use the `Auth` plugin. 

The constructor of the endpoint class gets a builder as a parameter, let's prepare the required one using the authorization plugin:

__BaseApiClient.kt__
```kotlin
protected val setupConfig: (HttpClientConfig<*>) -> Unit = { config -> getClientConfig(config) }

private fun getClientConfig(config: HttpClientConfig<*>) {
   config
       .install(Auth) {
           bearer {
               loadTokens { BearerTokens(ConfigurationProvider.token, refreshToken = "") }
           }
       }
}
```
`Ktor` generates asynchronous code, so all functions that call the API are marked with the `suspend` keyword:

__UserApiWrapper.kt__
```kotlin
class UserApiWrapper : BaseApiClient() {
   private val userApi = UserApi(host, httpClientConfig = setupConfig)

   // ****

   suspend fun createUser(): User {
      val newUser = User(
          username = "userName".appendRandomNumericPostfix(),
          name = "name".appendRandomNumericPostfix(),
          email = "${("fakeMail".appendRandomNumericPostfix())}@example.com",
          password = "pass".appendRandomNumericPostfix()
      )

      return userApi.addUser(body = newUser).body()
   }

   // ****
}
```
We get runtime errors while trying to run tests:
* Error `SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".` occurred because Ktor depends on SLF4J in runtime
* Error `Fail to prepare request body for sending. 
The body type is: class com.makrol.teamcity.api.client.model.User (Kotlin reflection is not available), with Content-Type: null.` occurred because we did not add content negotiation and did not pass content-type header

To fix the first one we need to add dependency. ([see in mvnrepository](https://mvnrepository.com/artifact/org.slf4j/slf4j-simple)):

__BaseApiClient.kt__
```kotlin
implementation("org.slf4j:slf4j-simple:$slf4jVersion")
```
To fix the second one, we need to configure content negotiation through the plugin and add the content type header. 
So we need to update the config builder:

__BaseApiClient.kt__
```kotlin
private fun getClientConfig(config: HttpClientConfig<*>) {
   config.install(DefaultRequest) {
       header("Content-Type", "application/json")
   }
   config
       .install(Auth) {
           bearer {
               loadTokens { BearerTokens(ConfigurationProvider.token, refreshToken = "") }
           }
       }
   config.install(ContentNegotiation) {
       json()
   }
}
```
We are faced with a `kotlinx.serialization.SerializationException`.

![Serialization exception](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s10jnbd72jl14d2hpdxx.png)
[If we read the kotlinx documentation](https://github.com/Kotlin/kotlinx.serialization#gradle-with-plugins-block), we would find that we need to install a corresponding plugin ([see in the mvnrepository](https://mvnrepository.com/artifact/org.jetbrains.kotlin.plugin.serialization/org.jetbrains.kotlin.plugin.serialization.gradle.plugin)). Let's add it:

__build.gradle.kts__
```kotlin
plugins {
   kotlin("jvm") version "1.8.0"
   kotlin("plugin.serialization") version "1.8.10"

   id("org.openapi.generator") version "6.3.0"
}
```
It works!

![Success with multiplatform](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b6730io0x0ayme6ga7ea.png)

##### Using `jvm-vertx` group to build: authorization is not supported <a name="change-libs-jvm-vertx"></a>
The last option available is `jvm-vertx`. [According to the documentation](https://openapi-generator.tech/docs/generators/kotlin-vertx/), the stability of the generator is "beta", but it [does not support authorization](https://openapi-generator.tech/docs/generators/kotlin-vertx/#client-modification-feature) yet. Since the API we are playing with is secured, the option is not available.

## Conclusion <a name="conclusion"></a>
As you can see, to get a successfully generated Kotlin client, you have to spend some time and play a bit with the settings.
The OpenAPI generator has rich configuration capabilities and it helps to solve a lot of problems, but there is no silver bullet and the settings I used to get a usable Kotlin client for TeamCity API may be useless in some other cases.
By changing the generator settings, you can change a whole set of libraries used to generate a client (option `library`) or you can change only the serializer (option `serializationLibrary`).
HTTP client was successfully built using `Ktor` and `OkHttp`. `Vert.x Web Client` does not support authorization for now, so it cannot be used for the API explored in the post.
JSON serialization has been successfully handled by kotlinx, gson, and jackson.
Decisions about which libraries to use must be made in the context of a task and project.
Using the `multiplatform` group (with `Ktor` and `Kotlinx` under the hood) seems most appropriate if the entire codebase of the project is written in Kotlin. Also, `Ktor` has a bit more flexible customization.

## Resources <a name="resources"></a>

* https://www.jetbrains.com/teamcity
* https://docs.gradle.org/current/userguide/getting_started.html
* https://github.com/OpenAPITools/openapi-generator/tree/master/docs 
* https://swagger.io/tools/swagger-codegen/
* https://square.github.io/okhttp/
* https://openapi-generator.tech/docs/configuration/
* https://openapi-generator.tech/docs/usage/
* https://ktor.io/learn/
* https://ktor.io/docs/getting-started-ktor-client.html
* https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md
* https://vertx.io/docs/vertx-core/kotlin/