A Gradle plugin that transforms OpenAPI v3 specifications into production-ready Kotlin Ktor client code.
The generated client code is fully KMP-compatible.
You can customize the generated clients and models to match your project's specific needs.
- JDK 17+
- Gradle 9+
Add the plugin to your build.gradle.kts:
plugins {
id("org.litote.openapi.ktor.client.generator.gradle") version "<last version>"
}Replace <last version> with the latest release:
Configure the plugin in your build.gradle.kts:
apiClientGenerator {
generators {
create("openapi") { // registers a task named generateOpenapi
outputDirectory = file("build/generated")
openApiFile = file("src/main/openapi/openapi.json")
basePackage = "com.example.api"
}
// you can declare multiple generators
}
}A full working example is available in e2e/build.gradle.kts.
Run the generation task directly:
./gradlew generateOpenapiOr let it run automatically as part of the build:
./gradlew buildThe generated code is placed in the configured outputDirectory. You also need to add Ktor, kotlinx-serialization, and kotlinx-coroutines to your dependencies for the project to compile.
The generator accepts OpenAPI V3 specification files in both JSON and YAML format.
After generation, each API tag produces a client class (e.g. UserClient, PetClient).
All clients share by default a single ClientConfiguration instance.
val config = ClientConfiguration()
val client = UserClient(config) // UserClient() uses default ClientConfiguration()
val users = client.getUsers()val config = ClientConfiguration(
baseUrl = "https://api.example.com/v1/",
logLevel = LogLevel.NONE, // silence HTTP logs
httpClientAuthorization = {
defaultRequest { header("Authorization", "Bearer $token") }
},
)
val client = UserClient(config)All parameters have sensible defaults — only override what you need.
| Parameter | Type | Default | Description |
|---|---|---|---|
baseUrl |
String |
value from spec | Base URL prepended to every request |
logLevel |
LogLevel |
LogLevel.HEADERS |
Ktor logging verbosity (ALL, HEADERS, BODY, INFO, NONE) |
engine |
HttpClientEngineFactory<*> |
CIO |
Ktor engine (swap for MockEngine in tests, OkHttp on Android, etc.) |
json |
Json |
Json { ignoreUnknownKeys = true } |
kotlinx.serialization Json instance |
httpClientAuthorization |
HttpClientConfig<*>.() -> Unit |
{} |
Hook to inject auth headers or other per-request config |
httpClientConfig |
HttpClientConfig<*>.() -> Unit |
defaultHttpClientConfig(…) |
Full Ktor client config — override to replace the default setup entirely |
client |
HttpClient |
built from engine + httpClientConfig |
Pre-built HttpClient — inject a mock in tests |
exceptionLogger |
Throwable.() -> Unit |
{ printStackTrace() } |
Called when a client catches an unexpected exception |
Tip: inject
client = mockClientin unit tests to avoid any network call.
| Property | Description | Default value | Allowed values |
|---|---|---|---|
generators |
One or more generator configurations | {} |
Any configuration |
skip |
Skip all client generation | false |
Boolean |
initSubproject |
Options for the initApiClientSubproject project generation task |
see PROJECT_GENERATION.md |
| Property | Description | Default value | Allowed values |
|---|---|---|---|
openApiFile |
OpenAPI v3 source file | file("src/main/openapi/${name}.json") |
Any existing OpenAPI file |
outputDirectory |
Target directory for generated sources (src/main/kotlin is appended automatically) |
file("build/api-${name}") |
Any relative directory |
basePackage |
Base package for all generated classes | org.example |
Any valid package name |
allowedPaths |
Restrict generation to a subset of OpenAPI paths | empty (all paths generated) | Any subset of paths defined in the spec |
modulesIds |
Built-in module IDs to enable (loaded from classpath via SPI) | empty | UnknownEnumValueModule, LoggingSl4jModule, LoggingKotlinModule, BasicAuthModule |
customModules |
Custom module instances defined inline in the build script | empty | Any ApiGeneratorModule implementation (see advanced usage) |
skip |
Skip this generator | false |
Boolean |
splitByClient |
Enable split-by-client mode — see PROJECT_GENERATION.md | false |
Boolean |
targetClientName |
In split mode: name of the client to generate (null = shared subproject) — see PROJECT_GENERATION.md |
null |
Any tag-derived client name from the spec |
The plugin provides an initApiClientSubproject task to generate a ready-to-use Gradle subproject.
See PROJECT_GENERATION.md for the full documentation: single/multi-module, version catalog support, extra generator configuration, and Kotlin Multiplatform (KMP) support.
Modules extend the code generator with additional behaviour. They are activated by adding their ID to modulesIds in the generator configuration:
apiClientGenerator {
generators {
create("openapi") {
openApiFile = file("src/main/openapi/openapi.json")
basePackage = "com.example.api"
modulesIds.add("UnknownEnumValueModule")
modulesIds.add("LoggingKotlinModule")
}
}
}| Module ID | Effect |
|---|---|
UnknownEnumValueModule |
Adds an UNKNOWN_ fallback constant to every generated enum and enables coerceInputValues = true in the Json configuration, so unknown server values never cause a deserialization error |
LoggingSl4jModule |
Configures the ClientConfiguration exception logger to use SLF4J (LoggerFactory.getLogger(…).error(…)). JVM-only — do not use in KMP projects targeting non-JVM platforms |
LoggingKotlinModule |
Configures the ClientConfiguration exception logger to use kotlin-logging / oshai (KotlinLogging.logger(…).error(…)) |
BasicAuthModule |
Adds an accessToken: String? parameter to ClientConfiguration and configures httpClientAuthorization to inject an Authorization: Bearer <token> header on every request |
You can define a module inline in your build.gradle.kts without publishing a separate artifact. Implement ApiGeneratorModule and pass it via customModules:
import org.litote.openapi.ktor.client.generator.ApiGeneratorModule
import org.litote.openapi.ktor.client.generator.domain.ClientSpec
import org.litote.openapi.ktor.client.generator.domain.GeneratedFileSpec
val copyrightModule = object : ApiGeneratorModule {
// Prepend a copyright header to every generated file
override fun transformFile(file: GeneratedFileSpec): GeneratedFileSpec =
file.copy(content = "// Copyright 2026 Acme Corp — do not edit\n" + file.content)
// Keep only GET operations in every client
override fun transformClientSpec(spec: ClientSpec): ClientSpec =
spec.copy(operations = spec.operations.filter { it.method == "GET" })
}
apiClientGenerator {
generators {
create("openapi") {
openApiFile = file("src/main/openapi/openapi.json")
basePackage = "com.example.api"
customModules.add(copyrightModule)
}
}
}Configuration cache compatibility: anonymous module instances are not serializable, so tasks that use
customModulesare not compatible with the Gradle configuration cache. Three alternatives:
buildSrcor a convention plugin — define the module as a named class there; Gradle can serialize it and the task stays configuration cache compatible:// buildSrc/src/main/kotlin/CopyrightModule.kt import org.litote.openapi.ktor.client.generator.ApiGeneratorModule import org.litote.openapi.ktor.client.generator.domain.GeneratedFileSpec class CopyrightModule : ApiGeneratorModule { override fun transformFile(file: GeneratedFileSpec): GeneratedFileSpec = file.copy(content = "// Copyright 2026 Acme Corp\n" + file.content) }// build.gradle.kts customModules.add(CopyrightModule())SPI via
modulesIds— package the module as a library with aMETA-INF/services/org.litote.openapi.ktor.client.generator.ApiGeneratorModuleentry, add it to the buildscript classpath, and reference it by ID. The built-in modules (UnknownEnumValueModule,LoggingSl4jModule,LoggingKotlinModule) follow exactly this pattern and can serve as implementation examples.Disable the configuration cache — if neither approach suits your project, set
org.gradle.configuration-cache=falseingradle.properties(this is the default value).
A module can implement any combination of the following hooks — all are no-ops by default:
| Hook | Called when | Can do |
|---|---|---|
processConfiguration(ApiConfigurationGeneratorConfig) |
Before ClientConfiguration is rendered |
Set custom Json properties (coerceInputValues, etc.), override the exception-logging lambda |
processClient(ApiClientGeneratorConfig) |
Before any client class is rendered | Configure client-level rendering options (reserved for future use) |
processModel(ApiModelGeneratorConfig) |
Before any model class is rendered | Set a fallback enum constant (defaultEnumValue) |
transformClientSpec(ClientSpec): ClientSpec |
For each client, before KotlinPoet rendering | Add, remove or rewrite operations; rename the client; change parameters or response types |
transformModelSpec(ModelSpec): ModelSpec |
For each model, before KotlinPoet rendering | Add, remove or rewrite properties; change the model kind (data class, enum, sealed…) |
transformFile(GeneratedFileSpec): GeneratedFileSpec |
After KotlinPoet rendering, before writing to disk | Add a file header, rewrite imports, inject code at the text level |
Hooks are applied in the order the modules are listed. transform* hooks receive an immutable domain object and must return the (possibly modified) replacement — the original is never mutated.
A version catalog is published to Maven Central alongside the plugin. It exposes all library versions used by the generator (Ktor, kotlinx.serialization, coroutines, etc.), which you can use to align your own dependencies.
In your settings.gradle.kts:
dependencyResolutionManagement {
versionCatalogs {
create("openapiKtor") {
from("org.litote.openapi.ktor.client.generator:version-catalog:<last version>")
}
}
}Then reference compatible versions in your build.gradle.kts:
dependencies {
implementation(openapiKtor.bundles.ktor)
implementation(openapiKtor.serialization)
implementation(openapiKtor.coroutines)
}If your OpenAPI spec uses application/yaml or application/x-yaml content types, the generator automatically:
- Generates a
YamlContentConverterclass in the client package - Registers it in
ContentNegotiationfor bothapplication/yamlandapplication/x-yaml - Sets the correct
Content-Typeheader on YAML requests
You must add SnakeYAML to your project dependencies:
dependencies {
implementation("org.yaml:snakeyaml:<latest version>")
}The latest SnakeYAML version can be found in the published version catalog (openapiKtor.versions.snakeyaml).
The YamlContentConverter (see below) generated when your API uses application/yaml content types depends on
SnakeYAML, which is a JVM-only library. If you
target non-JVM platforms, place the SnakeYAML dependency in a jvmMain source set and implement a
platform-specific YAML converter for other targets.
The LoggingSl4jModule generates code that uses org.slf4j.LoggerFactory, which is JVM-only.
Do not use this module in KMP projects targeting non-JVM platforms.
If you get a Gradle error about implicit task dependencies (see validation_problems#implicit_dependency), add the dependencies explicitly:
project
.tasks
.named { name -> name.contains("whatever") }
.configureEach {
project.tasks.withType(org.litote.openapi.ktor.client.generator.plugin.GenerateTask::class.java).forEach {
dependsOn(it)
}
}Generated code is not linted. Suppress linter warnings by adding an .editorconfig entry, for example for ktlint:
[build/**/*]
ktlint = disabled
See CONTRIBUTING.md for the hexagonal architecture diagram.