OpenAPI·Kotlin·Free

OpenAPI to
Kotlin.

Turn an OpenAPI 3.0 or 3.1 spec into a Kotlin client built on Ktor and kotlinx.serialization. Every oneOf becomes a @Serializable sealed interface tagged with @JsonClassDiscriminator, so polymorphic JSON decodes straight into the right variant — no custom serializer, no reflection. Every other schema is a @Serializable data class; every operation is a suspend fun on an HttpClient you own. No signup, no install.

OpenAPI 3.0 + 3.1·Kotlin·Ktor 3.x·kotlinx.serialization·MIT
payments.openapi.yamlInput
openapi: 3.0.3
info:
  title: Payments API
  version: 1.2.0
paths:
  /payments/{id}:
    get:
      operationId: getPayment
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: expand
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Payment'
components:
  schemas:
    Payment:
      type: object
      required: [id, amount, status, method]
      properties:
        id:       { type: string }
        amount:   { type: integer, format: int64 }
        currency: { type: string, nullable: true }
        status:   { $ref: '#/components/schemas/PaymentStatus' }
        method:
          oneOf:
            - $ref: '#/components/schemas/CardPayment'
            - $ref: '#/components/schemas/BankPayment'
          discriminator:
            propertyName: kind
            mapping:
              card: '#/components/schemas/CardPayment'
              bank: '#/components/schemas/BankPayment'
compile
com/example/payments/model/MethodUnion.kt tsc cleanOutput
// com/example/payments/model/MethodUnion.kt
package com.example.payments.model

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@Serializable
@JsonClassDiscriminator("kind")
sealed interface MethodUnion

// com/example/payments/model/CardPayment.kt
import kotlinx.serialization.SerialName

@Serializable
@SerialName("card")
data class CardPayment(val kind: String, val last4: String) : MethodUnion

// com/example/payments/model/BankPayment.kt
@Serializable
@SerialName("bank")
data class BankPayment(val kind: String, val iban: String) : MethodUnion

// Deserialize a payload — the discriminator routes
// to the right variant with no custom serializer:
val payment = Json.decodeFromString<Payment>(body)

when (payment.method) {
    is CardPayment -> charge(payment.method.last4)
    is BankPayment -> debit(payment.method.iban)
}
// Add a third variant to the spec and this 'when'
// stops compiling until the new branch is handled.
What gets generated

Data classes, a sealed interface, a suspend client.

Below: types emitted from a small payments spec with a oneOf discriminator and an enum. Each schema becomes a @Serializable data class; the oneOf becomes a sealed interface tagged with @JsonClassDiscriminator, with each variant marked @SerialName from the spec's mapping; enums preserve the wire value via @SerialName and ship a small KSerializer that routes unknown values to an UNKNOWN case so clients stay forward-compatible.

// com/example/payments/model/Payment.kt
package com.example.payments.model

import kotlinx.serialization.Serializable

@Serializable
data class Payment(
    val id: String,
    val amount: Long,
    val currency: String? = null,
    val status: PaymentStatus,
    val method: MethodUnion
)

// com/example/payments/model/MethodUnion.kt
package com.example.payments.model

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonClassDiscriminator

@Serializable
@JsonClassDiscriminator("kind")
sealed interface MethodUnion

// com/example/payments/model/CardPayment.kt
import kotlinx.serialization.SerialName

@Serializable
@SerialName("card")
data class CardPayment(val kind: String, val last4: String) : MethodUnion

// com/example/payments/model/BankPayment.kt
@Serializable
@SerialName("bank")
data class BankPayment(val kind: String, val iban: String) : MethodUnion

// com/example/payments/model/PaymentStatus.kt
@Serializable(with = PaymentStatusSerializer::class)
enum class PaymentStatus {
    @SerialName("authorized") AUTHORIZED,
    @SerialName("captured")   CAPTURED,
    @SerialName("refunded")   REFUNDED,
    @SerialName("failed")     FAILED,
    UNKNOWN
}

// A small KSerializer ships next to the enum so an
// unknown wire value decodes to UNKNOWN instead of
// throwing — your client stays forward-compatible
// when the server adds a new status.
Three things we get right

Grounded in how the spec reads.

Each claim here is something you can verify by running the converter on your own spec. Click a card to see the spec fragment that triggered it and the Kotlin that came out.

01ProofSealed interface for every oneOf.Spec · InCode · Out
spec.yaml fragment
method:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankPayment'
  discriminator:
    propertyName: kind
    mapping:
      card: '#/components/schemas/CardPayment'
      bank: '#/components/schemas/BankPayment'
emitted + call-site
// model/MethodUnion.kt
@Serializable
@JsonClassDiscriminator("kind")
sealed interface MethodUnion

// model/CardPayment.kt
@Serializable
@SerialName("card")
data class CardPayment(val kind: String, val last4: String) : MethodUnion

// model/BankPayment.kt
@Serializable
@SerialName("bank")
data class BankPayment(val kind: String, val iban: String) : MethodUnion

// Decode a JSON payload — the discriminator
// routes to the right variant automatically:
val payment = Json.decodeFromString<Payment>(body)

// Pattern-match at the call site:
val label = when (payment.method) {
    is CardPayment -> "card ${payment.method.last4}"
    is BankPayment -> "bank ${payment.method.iban}"
}
// Add a third variant to the spec and this 'when'
// stops compiling until the new branch is handled.
Kotlin · Ktor 3.x

One HttpClient. Every generated service.

You own the HttpClient. Pick the engine (CIO on JVM, OkHttp on Android, Darwin on iOS, JS in browsers), install ContentNegotiation with Json(), Auth, Logging, and HttpTimeout once, and share it across every generated client. Nothing in the generated output reaches into that layer, and nothing has to change when you swap engines or add a plugin.

com/example/payments/HttpClientFactory.kt generated
// com/example/payments/HttpClientFactory.kt
package com.example.payments

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*

// One HttpClient. Share it across every generated client.
val httpClient = HttpClient(CIO) {
    install(ContentNegotiation) {
        json()  // kotlinx.serialization — decodes your models
    }
    install(Auth) {
        bearer {
            loadTokens { BearerTokens(accessToken, refreshToken) }
            refreshTokens { /* refresh flow */ }
        }
    }
    install(Logging) { level = LogLevel.INFO }
    install(HttpTimeout) {
        requestTimeoutMillis = 10_000
        connectTimeoutMillis = 5_000
    }
}

val payments = DefaultClient(httpClient, "https://api.example.com")
val orders   = OrdersClient(httpClient, "https://api.example.com")

// Swap CIO for OkHttp on Android, Darwin on iOS, JS on
// browsers — the generated client files don't change.
Also available via MCP

Or ask Claude to do it.

The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a spec and it produces the Kotlin client as a diff — free, no token, no leaving the chat.

// claude-desktop config · .mcp.json
{
  "mcpServers": {
    "metaengine": {
      "command": "npx",
      "args": ["-y", "@metaengine/mcp-server"]
    }
  }
}

// Then in Claude:
// "Load specs/payments.yaml and generate a Kotlin
//  Ktor client in package com.example.payments."
See the MCP page
Prefer it in your build?

Same generator, as a package.

The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.

Questions

Things people ask.

The questions that come up most when generating Kotlin clients from OpenAPI. If yours is not here, run the converter — the answer is usually in the output.

A package of .kt files: one @Serializable data class per schema, one @Serializable enum class per string enum, a sealed interface per oneOf with the variants implementing it, and one client class per tag (DefaultClient when no tags are set). Each operation is a suspend fun that takes and returns the model types directly.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIReact
TanStack Query hooks with stable query keys, or plain async functions.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIJava Spring
RestClient services, Java records, sealed interfaces, Spring Boot 3.
Open converter
OpenAPIPython
Async httpx clients paired with Pydantic v2 models.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
GraphQLC#
Records, nullable types, [JsonPolymorphic] unions, IAsyncEnumerable subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
ProtobufGo
Plain structs with json tags, pointer presence, stdlib-only Connect client.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own spec.

The converter runs in the browser. Your spec never leaves the page.

Open the converter