Ktor 3.4.0 Help

OpenAPI specification generation

Ktor provides support for building OpenAPI specifications at runtime from one or more documentation sources.

This functionality is available through:

  • The OpenAPI compiler extension (included in the Ktor Gradle plugin), which analyzes routing code at compile time and generates Kotlin code that registers OpenAPI metadata at runtime.

  • The routing annotation runtime API, which attaches OpenAPI metadata directly to routes in the running application.

You can use one or both and combine them with the OpenAPI and SwaggerUI plugins to serve interactive API documentation.

Add dependencies

  • To enable OpenAPI metadata generation, apply the Ktor Gradle plugin to your project:

plugins { id("io.ktor.plugin") version "3.4.0" }
  • To use runtime route annotations, add the ktor-server-routing-openapi artifact to your build script:

    implementation("io.ktor:ktor-server-routing-openapi:$ktor_version")
    implementation "io.ktor:ktor-server-routing-openapi:$ktor_version"
    <dependency> <groupId>io.ktor</groupId> <artifactId>ktor-server-routing-openapi-jvm</artifactId> <version>${ktor_version}</version> </dependency>

Configure the OpenAPI compiler extension

The OpenAPI compiler extension controls how routing metadata is collected at compile time. It does not define the final OpenAPI document itself.

During compilation, the plugin generates Kotlin code that uses the OpenAPI runtime API to register metadata derived from routing declarations, code patterns, and comments.

General OpenAPI information — such as the API title, version, servers, security schemes, and detailed schemas — is supplied at runtime when the specification is generated.

To configure the compiler plugin extension, use the openApi {} block inside the ktor extension in your build.gradle.kts file:

ktor { openApi { enabled = true codeInferenceEnabled = true onlyCommented = false } }

Configuration options

enabled

Enables or disables OpenAPI route annotation code generation. Defaults to false.

codeInferenceEnabled

Controls whether the compiler attempts to infer OpenAPI metadata from routing code. Defaults to true. Disable this option if inference produces incorrect results, or you prefer to define metadata explicitly using annotations. For more details, see code inference rules.

onlyCommented

Limits metadata generation to routes that contain comment annotations. Defaults to false, meaning all routing calls are processed except those explicitly marked with @ignore.

Routing structure analysis

The Ktor compiler plugin analyzes your server routing DSL to determine the structural shape of your API. This analysis is based solely on route declarations and does not inspect the contents of route handlers.

The following is automatically inferred from the selectors in the routing API tree:

  • Merged paths (for example, /api/v1/users/{id}).

  • HTTP methods (such as GET and POST).

  • Path parameters.

routing { route("/api/v1") { get("/users") { } get("/users/{id}") { } post("/users") { } } }

Because request parameters, bodies, and responses are handled inside route lambdas, the compiler cannot infer a complete OpenAPI description from the routing structure alone. To enrich the generated metadata, Ktor supports annotations and automatic inference based on common request-handling patterns.

Code inference

When code inference is enabled, the compiler plugin recognizes common Ktor usage patterns and generates equivalent runtime annotations automatically.

The following table summarizes the supported inference rules:

Rule

Description

Input

Output (from annotate scope)

Request Body

Provides request body schema from ContentNegotiation reads

call.receive<T>()

requestBody { schema = jsonSchema<T>() }

Response Body

Provides response body schema from ContentNegotiation writes

call.respond<T>()

responses { HttpStatusCode.OK { schema = jsonSchema<T>() } }

Response Headers

Includes custom headers for responses

call.response.header("X-Foo", "Bar")

responses { HttpStatusCode.OK { headers { header("X-Foo", "Bar") } } }

Path Parameters

Finds path parameter references

call.parameters["id"]

parameters { path("id") }

Query Parameters

Finds query parameter references

call.queryParameters["name"]

parameters { query("name") }

Request Headers

Finds request header references

call.request.headers["X-Foo"]

parameters { header("X-Foo") }

Resource API routes

Infers call structure for the Resources routing API

call.get<List> { /**/ }; @Resource("/list") class List(val name: String)

parameters { query("name") }

Inference follows extracted functions where possible and attempts to generate consistent documentation for typical request and response flows.

Disable inference for an endpoint

If inference produces incorrect metadata for a specific endpoint, you can exclude it by adding an ignore marker:

// ignore! get("/comments") { // ... }

Annotate routes

To enrich the specification, Ktor supports two ways of annotating routes:

You can use either approach or combine both.

Comment-based route annotations

Comment-based annotations provide metadata that cannot be inferred from code and integrate seamlessly with existing routes.

Metadata is defined by placing a keyword at the start of a line, followed by a colon (:) and its value.

You can attach comments directly to route declarations:

/** * Get a single user by ID. * * Path: id [ULong] the ID of the user * * Responses: * – 400 The ID parameter is malformatted or missing. * – 404 The user for the given ID does not exist. * – 200 [User] The user found with the given ID. */ get("/{id}") { val id = call.parameters["id"]?.toULongOrNull() ?: return@get call.respond(HttpStatusCode.BadRequest) val user = list.find { it.id == id } ?: return@get call.respond(HttpStatusCode.NotFound) call.respond(user) }

Formatting rules

  • Keywords must appear at the start of the line.

  • A colon (:) separates the keyword from its value.

  • Plural forms (for example, Tags, Responses) allow grouped definitions.

  • Singular forms (for example, Tag, Response) are also supported.

  • Top-level bullet points (-) are optional and only affect formatting.

The following variants are equivalent:

/** * Tag: widgets * * Tags: * - widgets * * - Tags: * - widgets */

Supported comment fields

Tag

Format

Description

Tag

Tag: name

Groups the endpoint under a tag

Path

Path: [Type] name description

Path parameter

Query

Query: [Type] name description

Query parameter

Header

Header: [Type] name description

Header parameter

Cookie

Cookie: [Type] name description

Cookie parameter

Body

Body: contentType [Type] description

Request body

Response

Response: code contentType [Type] description

Response definition

Deprecated

Deprecated: reason

Marks the endpoint as deprecated

Description

Description: text

Extended description

Security

Security: scheme

Security requirements

ExternalDocs

ExternalDocs: href

External documentation link

Runtime route annotations

In cases where compile-time analysis is insufficient, such as when using dynamic routing, interceptors, or conditional logic, you can attach OpenAPI operation metadata directly to a route at runtime using the .describe {} extension function.

Each annotated route defines an OpenAPI Operation object, which represents a single HTTP operation (for example, GET /users) in the generated OpenAPI specification. The metadata is attached to the routing tree at runtime and is consumed by the OpenAPI and Swagger UI plugins.

The .describe {} DSL maps directly to the OpenAPI specification. Property names and structure correspond to the fields defined for an Operation object, including parameters, request bodies, responses, security requirements, servers, callbacks, and specification extensions (x-*).

The runtime route annotations API is experimental and requires opting in using @OptIn(ExperimentalKtorApi::class):

@OptIn(ExperimentalKtorApi::class) get("/users") { val query = call.parameters["q"] val result = if (query != null) { list.filter {it.name.contains(query, ignoreCase = true) } } else { list } call.respond(result) }.describe { summary = "Get users" description = "Retrieves a list of users." parameters { query("q") { description = "An encoded query" required = false } } responses { HttpStatusCode.OK { description = "A list of users" schema = jsonSchema<List<User>>() } HttpStatusCode.BadRequest { description = "Invalid query" ContentType.Text.Plain() } } }

Runtime annotations are merged with compiler-generated and comment-based metadata. When the same OpenAPI field is defined by multiple sources, values provided by runtime annotations take precedence.

Hide routes from the OpenAPI specification

To exclude a route and its children from the generated OpenAPI document, use the Route.hide() function:

@OptIn(ExperimentalKtorApi::class) get("/routes") { // .... }.hide()

This is useful for internal, administrative, or diagnostic endpoints that should not be published, including routes used to generate the OpenAPI specification itself.

The OpenAPI and Swagger UI plugins call .hide() automatically, so their routes are excluded from the resulting document.

Generate and serve the specification

The OpenAPI specification is assembled at runtime from runtime route annotations and metadata generated by the compiler plugin.

You can expose the specification in the following ways:

Assemble and serve the specification

To assemble a complete OpenAPI document at runtime, create an OpenApiDoc instance and provide the routes that should be included in the specification.

The document is assembled from compiler-generated metadata and runtime route annotations from the routing tree. The resulting OpenApiDoc instance always reflects the current state of the application.

You typically construct the document from a route handler and respond with it directly:

get("/docs.json") { val doc = OpenApiDoc(info = OpenApiInfo("My API", "1.0")) + apiRoute.descendants() call.respond(doc) }

In this example, the OpenAPI document is serialized using the ContentNegotiation plugin. This assumes that a JSON serializer (for example, kotlinx.serialization) is installed.

No additional build or generation step is required. Changes to routes or annotations are reflected automatically the next time the specification is requested.

Serve interactive documentation

To expose the OpenAPI specification through an interactive UI, use the OpenAPI and Swagger UI plugins.

Both plugins assemble the specification at runtime and can read metadata directly from the routing tree. They differ in how the documentation is rendered:

  • The OpenAPI plugin renders documentation on the server and serves pre-generated HTML.

  • The Swagger UI plugin serves the OpenAPI specification as JSON or YAML and renders the UI in the browser using Swagger UI.

// Serves the OpenAPI UI openAPI("/openApi") // Serves the Swagger UI swaggerUI("/swaggerUI") { info = OpenApiInfo("My API", "1.0") source = OpenApiDocSource.RoutingSource(ContentType.Application.Json) { apiRoute.descendants() } }

Metadata precedence

The final OpenAPI specification is assembled at runtime by merging metadata contributed from multiple sources.

The following sources are applied, in order:

  1. Compiler-generated metadata, including:

  2. Comment-based route annotations

  3. Runtime route annotations

When the same OpenAPI field is defined by multiple sources, values provided by runtime annotations take precedence over comment-based annotations and compiler-generated metadata.

Metadata that is not explicitly overridden is preserved and merged into the final document.

23 January 2026