This project aims to generate easy-to-use and portable Ktor clients from OpenApi V3 yaml specifications.
libs.version.toml
openapi = { id = "com.dshatz.openapi2ktor", version = "1.0.7" }build.gradle.kts
plugins {
// your kotlin plugin - jvm or multiplatform
alias(libs.plugins.openapi)
// If you are not using version catalog
id("com.dshatz.openapi2ktor") version "1.0.7"
}Also include ktor client and kotlin serialization.
openapi3 {
generators {
// Give your api a name.
create("binance") {
inputSpec.set(file("openapi/binance.yaml")) // This should be a module-relative path.
}
}
}A gradle task will be added under group openapi3 with the following name:
generate<name>Clients.
In the above example, it is generateBinanceClients.
This task will be automatically executed when there are any changes to the input spec file.
You can customize some of the behaviour of the generators. Please open an issue if you need additional configuration.
By default, both models and clients will be generated. To generate only kotlin models:
create("example") {
inputSpec.set(file("..."))
config {
generateClients(false)
}
}By default, date and date-time fields will be generated as Strings.
You can configure this behaviour:
create("example") {
inputSpec.set(file("..."))
config {
generateClients(false)
dateLibrary(DateLibrary.JavaTime)
// or dateLibrary(DateLibrary.KotlinxDatetime)
// or default dateLibrary(DateLibrary.String)
}
}Both kotlinx datetime and Java time will have ISO8601 serializers applied. For example, these strings will be valid:
"2035-02-02T08:00:00+0300""2025-02-02T08:00:00Z""2025-02-02"(for property withformat: date)
And these invalid:
"2035-02-02T08:00:00"
To support other formats, please create an issue. In the meantime, use DateLibrary.String and implement your own parsing.
Some openapi spec files you find on the internet are not exactly complete. In case the API gives you an object with an unknown field, it will be dropped. Kotlinx Serialization does not support this out of the box, but this plugin gives you a possibility to not lose those fields.
To enable this on per-url basis:
openapi {
generators {
// Give your api a name.
create("binance") {
inputSpec.set(file("openapi/binance.yaml")) // This should be a module-relative path.
config {
parseUnknownProps {
all() // To parse additional properties on all urls.
urlIs("/your/url")
urlStartsWith("/your-url-prefix")
regex("[your url regex]")
}
}
}
}
}The generated response models of marked urls will then have a property additionalProps: Map<String, JsonElement>.
Note: this may have a performance penalty so only enable on urls which are known to misalign with the API spec.
Note: It is recommended to inspect module/build/openapi/<name>/client directory to know what clients have been generated.
The generated client constructor's signature is identical to that of ktor's HttpClient except that it also takes an optional baseUrl argument.
You can access the baseUrls through <name>.client.Servers enum or pass a custom String.
Important
To customize Json format, pass your own kotlinx.serialization.Json instance to the client constructor.
val apiClient = V3Client(engine = CIO)
val apiClientStaging = V3Client(engine = CIO, baseUrl = "https://staging.example.com")
val apiClientProduction = V3Client(engine = CIO, baseUrl = Servers.PRODUCTION, json = Json { ignoreUnknownKeys = true ))| Scheme | Supported |
|---|---|
| Bearer | ✅ |
| Api key | ✅ |
| Basic | ✅ |
| OAuth | ❌ |
To apply your authentication information, use the method that starts with set in on your client instance. The exact name depends on the securityScheme name in the YAML file.
Read this to know how to call your API.
Note: All methods are suspending.
The return type of all methods is always HttpResult<D, E: Exception>.
Both D and E are Kotlin classes representing success and error response models respectively.
If the YAML specifies multiple possible response models (depending on HTTP status code), D and E will be sealed classes.
Using Binance as an example.
val client = V3Client(CIO)
val response: HttpResult<GetApiV3AvgPriceResponse, GetApiV3AvgPriceResponse400> = client.getAvgPrice("BTCEUR")
when (response) {
is GetApiV3AvgPriceResponse -> {
println("BTC price: ${response.price} EUR")
}
is GetApiV3AvgPriceResponse400 -> {
println("Status code 400 error: ${response.data.code}")
}
}The generated code may throw a couple of errors:
UnknownSuccessCodeErrorif API returned a success code that is not described in the YAML. Thebody: Stringproperty will have the original body.io.ktor.client.plugins.ClientRequestExceptionif API returned a 4xx code that is not described in the YAML.RedirectResponseExceptionfor 3xx, andServerResponseExceptionfor 5xx - see https://ktor.io/docs/client-response-validation.html
dataOrNull Returns the success response object or null.
val response: HttpResult<GetApiV3AvgPriceResponse, GetApiV3AvgPriceResponse400> = client.getAvgPrice("BTCEUR")
val data: GetApiV3AvgPriceResponse? = response.dataOrNull()dataOrThrow Returns the success response or throws the error response object as an exception.
val response: HttpResult<GetApiV3AvgPriceResponse, GetApiV3AvgPriceResponse400> = client.getAvgPrice("BTCEUR")
try {
response.dataOrThrow()
} catch (e: GetApiV3AvgPriceResponse400) {
println("Error occured: ${e.data.code}")
}| Feature | Supported | Notes |
|---|---|---|
| type: number | ✅ | generates either float or int |
| type: boolean | ✅ | generates Boolean |
| type: array | ✅ | generates List and T from items schema |
| type: object | ✅ | generates a kotlin data class |
| oneOf multiple objects | ✅ | generates a sealed class |
| oneOf objects and primitives | ❌ | becomes a JsonElement |
| oneOf primitives | ❌ | becomes a JsonPrimitive |
| allOf | ✅ | generates a summary kotlin data class with fields from all of the subtypes |
| enums | ✅ | enum class |
| anyOf | ❌ | becomes a JsonElement |
| description | ✅ | generates simple KDocs |
| urls with multiple possible status codes | ✅ | generates a sealed interface of possible responses. Use when() to process. |
Give it a try and let me know how it goes. I still expect some OpenAPI specs not to compile, so please attach those if this happens.