diff --git a/codeSnippets/build.gradle b/codeSnippets/build.gradle index 33a66ee64..7c20e78f3 100644 --- a/codeSnippets/build.gradle +++ b/codeSnippets/build.gradle @@ -16,7 +16,7 @@ buildscript { } repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } configurations.classpath { @@ -70,14 +70,14 @@ allprojects { kotlin_version = rootProject.properties['kotlin_snapshot_version'] repositories { mavenLocal() - maven { url "https://oss.sonatype.org/content/repositories/snapshots" } + maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") } } } repositories { mavenCentral() maven { - url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev" + url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") } } } @@ -89,7 +89,7 @@ def ktorRepositoryDir = file("$buildDir/m2") if (ktorRepositoryDir.exists()) { allprojects { repositories { - maven { url ktorRepositoryDir.absolutePath } + maven { url = uri(ktorRepositoryDir.absolutePath) } } } } else { diff --git a/codeSnippets/gradle.properties b/codeSnippets/gradle.properties index dc49476a6..f5c1b9fd1 100644 --- a/codeSnippets/gradle.properties +++ b/codeSnippets/gradle.properties @@ -4,9 +4,11 @@ kotlin.code.style = official kotlin.native.binary.memoryModel = experimental # gradle configuration org.gradle.configureondemand = false +kotlin.mpp.applyDefaultHierarchyTemplate=false +org.gradle.java.installations.auto-download=false # versions kotlin_version = 2.2.20 -ktor_version = 3.4.0-eap-1477 +ktor_version = 3.3.3 kotlinx_coroutines_version = 1.10.1 kotlinx_serialization_version = 1.8.0 kotlin_css_version = 1.0.0-pre.721 diff --git a/codeSnippets/settings.gradle.kts b/codeSnippets/settings.gradle.kts index fc1e6f2e7..742f8d072 100644 --- a/codeSnippets/settings.gradle.kts +++ b/codeSnippets/settings.gradle.kts @@ -162,6 +162,7 @@ module("snippets", "tutorial-server-websockets") module("snippets", "tutorial-server-docker-compose") module("snippets", "htmx-integration") module("snippets", "server-http-request-lifecycle") +module("snippets", "openapi-spec-gen") if(!System.getProperty("os.name").startsWith("Windows")) { module("snippets", "embedded-server-native") diff --git a/codeSnippets/snippets/openapi-spec-gen/README.md b/codeSnippets/snippets/openapi-spec-gen/README.md new file mode 100644 index 000000000..fa096e443 --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/README.md @@ -0,0 +1,20 @@ +# OpenAPI documentation + +A sample Ktor project showing how to build OpenAPI documentation using routing annotations and the compiler +extension of the Ktor Gradle plugin. + +> This sample is a part of the [`codeSnippets`](../../README.md) Gradle project. + +## Run the application + +To run the application, execute the following command in the repository's root directory: + +```bash +./gradlew :openapi-spec-gen:run +``` + +To view the OpenAPI documentation, navigate to the following URLs: + +- [http://0.0.0.0:8080/docs.json](http://0.0.0.0:8080/docs.json) to view a JSON document of the API spec. +- [http://0.0.0.0:8080/openApi](http://0.0.0.0:8080/openApi) to view the OpenAPI UI for the API spec. +- [http://0.0.0.0:8080/swaggerUI](http://0.0.0.0:8080/swaggerUI) to view the Swagger UI for the API spec. diff --git a/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts b/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts new file mode 100644 index 000000000..48ad8f21a --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/build.gradle.kts @@ -0,0 +1,40 @@ +val ktor_version = "3.4.0-eap-1518" +val kotlin_version: String by project +val logback_version: String by project + +plugins { + application + kotlin("jvm") + id("io.ktor.plugin") version "3.3.3" +} + +application { + mainClass = "io.ktor.server.netty.EngineMain" +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap") +} + +ktor { + openApi { + enabled = true + codeInferenceEnabled = true + onlyCommented = false + } +} + + +dependencies { + implementation("io.ktor:ktor-server-core:$ktor_version") + implementation("io.ktor:ktor-server-routing-openapi:$ktor_version") + implementation("io.ktor:ktor-server-openapi:$ktor_version") + implementation("io.ktor:ktor-server-content-negotiation:${ktor_version}") + implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}") + implementation("io.ktor:ktor-server-swagger:${ktor_version}") + implementation("io.ktor:ktor-server-netty:$ktor_version") + implementation("ch.qos.logback:logback-classic:$logback_version") + testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version") + testImplementation("org.jetbrains.kotlin:kotlin-test") +} diff --git a/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt b/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt new file mode 100644 index 000000000..5a491f8f8 --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt @@ -0,0 +1,163 @@ +package com.example + +import io.ktor.server.routing.openapi.OpenApiDocSource +import io.ktor.server.routing.openapi.describe +import io.ktor.http.* +import io.ktor.openapi.OpenApiDoc +import io.ktor.openapi.OpenApiInfo +import io.ktor.openapi.jsonSchema +import io.ktor.serialization.kotlinx.json.json +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.ContentNegotiation +import io.ktor.server.plugins.openapi.* +import io.ktor.server.plugins.swagger.swaggerUI +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import io.ktor.utils.io.ExperimentalKtorApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module() { + install(ContentNegotiation) { + json(Json { + encodeDefaults = false + }) + } + @OptIn(ExperimentalKtorApi::class) + routing { + // Main page for marketing + get("/") { + call.respondText("

Hello, World

", ContentType.Text.Html) + } + + /** + * API endpoints for users. + * + * These will appear in the resulting OpenAPI document. + */ + val apiRoute = userCrud() + + get("/docs.json") { + val doc = OpenApiDoc(info = OpenApiInfo("My API", "1.0") + apiRoute.descendants()) + call.respond(doc) + } + + /** + * View the generated UI for the API spec. + */ + openAPI("/openApi"){ + outputPath = "docs/routes" + // title, version, etc. + info = OpenApiInfo("My API from routes", "1.0.0") + // which routes to read from to build the model + // by default, it checks for `openapi/documentation.yaml` then use the routing root as a fallback + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + apiRoute.descendants() + } + } + + /** + * View the Swagger flavor of the UI for the API spec. + */ + swaggerUI("/swaggerUI") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + apiRoute.descendants() + } + } + } +} + +fun Routing.userCrud(): Route = + route("/api") { + route("/users") { + val list = mutableListOf() + + /** + * 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) + } + + /** + * Get a list of users. + * + * – Response: 200 The list of items. + */ + @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>() + } + HttpStatusCode.BadRequest { + description = "Invalid query" + ContentType.Text.Plain() + } + } + } + + /** + * Save a new user. + * + * – Response: 204 The new user was saved. + */ + post { + list += call.receive() + call.respond(HttpStatusCode.NoContent) + } + + /** + * Delete the user with the given ID. + * + * – Path id [ULong] the ID of the user to remove + * – Response: 400 The ID parameter is malformatted or missing. + * – Response: 404 The user for the given ID does not exist. + * – Response: 204 The user was deleted. + */ + delete("/{id}") { + val id = call.parameters["id"]?.toULongOrNull() + ?: return@delete call.respond(HttpStatusCode.BadRequest) + if (!list.removeIf { it.id == id }) + return@delete call.respond(HttpStatusCode.NotFound) + call.respond(HttpStatusCode.NoContent) + } + + } +} + +@Serializable +data class User(val id: ULong, val name: String) diff --git a/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf new file mode 100644 index 000000000..2d8cb23be --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/application.conf @@ -0,0 +1,8 @@ +ktor { + deployment { + port = 8080 + } + application { + modules = [ com.example.ApplicationKt.module ] + } +} \ No newline at end of file diff --git a/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml new file mode 100644 index 000000000..05f2549ee --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/main/resources/logback.xml @@ -0,0 +1,12 @@ + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt b/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt new file mode 100644 index 000000000..2236d246b --- /dev/null +++ b/codeSnippets/snippets/openapi-spec-gen/src/test/kotlin/ApplicationTest.kt @@ -0,0 +1,14 @@ +package com.example + +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.testing.* +import kotlin.test.* + +class ApplicationTest { + @Test + fun testRoot() = testApplication { + } +} diff --git a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml index 2e7fe41aa..da4fe9258 100644 --- a/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml +++ b/codeSnippets/snippets/tutorial-server-db-integration/gradle/libs.versions.toml @@ -23,7 +23,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" } ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin-version" } -ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation-jvm", version.ref = "ktor-version" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-version" } [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" } diff --git a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts index 2c26eb7c2..5134f82f5 100644 --- a/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-docker-compose/build.gradle.kts @@ -39,6 +39,6 @@ dependencies { implementation("ch.qos.logback:logback-classic:$logback_version") implementation("io.ktor:ktor-server-config-yaml:$ktor_version") testImplementation("io.ktor:ktor-server-test-host-jvm") - testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") } diff --git a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts index b06e12807..8ddaf9971 100644 --- a/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts +++ b/codeSnippets/snippets/tutorial-server-websockets/build.gradle.kts @@ -32,5 +32,5 @@ dependencies { implementation("ch.qos.logback:logback-classic:$logback_version") testImplementation("io.ktor:ktor-server-test-host-jvm") testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version") - testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version") + testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version") } diff --git a/labels.list b/labels.list index e4138ff33..7e15ed171 100644 --- a/labels.list +++ b/labels.list @@ -9,14 +9,12 @@ Server Plugin - + This feature is experimental. It may be dropped or changed at any time. Opt-in is required (see details below). Server Work in progress Beta - - This is an experimental feature New in version 2023.3 \ No newline at end of file diff --git a/topics/openapi-spec-generation.md b/topics/openapi-spec-generation.md index 960f83f54..58766b015 100644 --- a/topics/openapi-spec-generation.md +++ b/topics/openapi-spec-generation.md @@ -1,18 +1,29 @@ [//]: # (title: OpenAPI specification generation) - + + +

+Required dependencies: io.ktor:%artifact_name% +

+

Code example: openapi

-Ktor provides experimental support for generating OpenAPI specifications directly from your Kotlin code. -This functionality is available via the Ktor Gradle plugin and can be combined with the [OpenAPI](server-openapi.md) +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](server-openapi.md) and [SwaggerUI](server-swagger-ui.md) plugins to serve interactive API documentation. > The OpenAPI Gradle extension requires Kotlin 2.2.20. Using other versions may result in compilation @@ -20,9 +31,9 @@ and [SwaggerUI](server-swagger-ui.md) plugins to serve interactive API documenta > {style="note"} -## Add the Gradle plugin +## Add dependencies -To enable specification generation, apply the Ktor Gradle plugin to your project: +* To enable OpenAPI metadata generation, apply the Ktor Gradle plugin to your project: ```kotlin plugins { @@ -30,37 +41,59 @@ plugins { } ``` -## Configure the extension +* To use runtime route annotations, add the `%artifact_name%` artifact to your build script: -To configure the extension, use the `openApi` block inside the `ktor` extension in your -build.gradle.kts -file. You can provide metadata such as title, description, license, and contact information: + -```kotlin -ktor { - @OptIn(OpenApiPreview::class) - openApi { - title = "OpenAPI example" - version = "2.1" - summary = "This is a sample API" - description = "This is a longer description" - termsOfService = "https://example.com/terms/" - contact = "contact@example.com" - license = "Apache/1.0" - - // Location of the generated specification (defaults to openapi/generated.json) - target = project.layout.buildDirectory.file("open-api.json") - } -} -``` +## Configure the OpenAPI compiler extension {id="configure-the-extension"} -## Routing API introspection +The OpenAPI compiler extension controls how routing metadata is collected at compile time. +It does not define the final OpenAPI document itself. -The plugin can analyze your server routing DSL to infer basic path information, such as: +During compilation, the plugin generates Kotlin code that uses the OpenAPI runtime API to register metadata derived +from routing declarations, code patterns, and comments. -- The merged path (`/api/v1/users/{id}`). -- Path parameters. +General OpenAPI information — such as the API title, version, servers, security schemes, and detailed schemas — is supplied +at runtime [when the specification is generated](#generate-and-serve-the-specification). + +To configure the compiler plugin extension, use the `openApi {}` block inside the `ktor` extension in your +build.gradle.kts +file: + +```kotlin +``` +{src="snippets/openapi-spec-gen/build.gradle.kts" include-lines="20-26"} + +### Configuration options + + + +<code>enabled</code> +Enables or disables OpenAPI route annotation code generation. Defaults to false. + + +<code>codeInferenceEnabled</code> +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. + + +<code>onlyCommented</code> +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. ```kotlin routing { @@ -72,72 +105,228 @@ routing { } ``` -Because request parameters and responses are handled inside route lambdas, the plugin cannot infer detailed -request/response schemas automatically. To generate a complete and useful specification, you can use annotations. +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](#annotate-routes) and [automatic inference](#code-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. -## Annotate routes +The following table summarizes the supported inference rules: -To enrich the specification, Ktor uses a KDoc-like annotation API. Annotations provide metadata that cannot be inferred -from code and integrate seamlessly with existing routes. +| Rule | Description | Input | Output (from annotate scope) | +|---------------------|----------------------------------------------------------------|----------------------------------------------------------------------------|--------------------------------------------------------------------------| +| Request Body | Provides request body schema from `ContentNegotiation` reads | `call.receive()` | `requestBody { schema = jsonSchema() }` | +| Response Body | Provides response body schema from `ContentNegotiation` writes | `call.respond()` | `responses { HttpStatusCode.OK { schema = jsonSchema() } }` | +| 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 { /**/ }; @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: ```kotlin -/** - * Get a single user. - * - * @path id The ID of the user - * @response 404 The user was not found - * @response 200 [User] The user. - */ -get("/api/users/{id}") { - val user = repository.get(call.parameters["id"]!!) - ?: return@get call.respond(HttpStatusCode.NotFound) - call.respond(user) +// ignore! +get("/comments") { + // ... } +``` + +## Annotate routes {id="annotate-routes"} + +To enrich the specification, Ktor supports two ways of annotating routes: + +- [Comment-based annotations](#comment-annotations), analyzed by the compiler plugin. +- [Runtime route annotations](#runtime-route-annotations), defined using the `.describe {}` DSL. + +You can use either approach or combine both. +### Comment-based route annotations {id="comment-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: + +```kotlin ``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="79-95"} -### Supported KDoc fields +#### Formatting rules -| Tag | Format | Description | -|-----------------|-------------------------------------------------|-------------------------------------------------| -| `@tag` | `@tag *name` | Associates the endpoint with a tag for grouping | -| `@path` | `@path [Type] name description` | Describes a 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 with optional type | -| `@deprecated` | `@deprecated reason` | Marks endpoint deprecated | -| `@description` | `@description text` | Extended description | -| `@security` | `@security scheme` | Security requirements | -| `@externalDocs` | `@external href` | External documentation links | +- 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: -## Generate the specification +```kotlin +/** + * Tag: widgets + * + * Tags: + * - widgets + * + * - Tags: + * - widgets + */ +``` -To generate the OpenAPI specification, run the following Gradle task: +#### Supported comment fields -```shell -./gradlew buildOpenApi +| 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 {id="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](https://swagger.io/specification/#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)`: + +```kotlin ``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="102-131"} + +> For a complete list of available fields, refer to the [OpenAPI specification](https://swagger.io/specification/#operation-object). +> +{style="tip"} + +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](#metadata-precedence). + +## Hide routes from the OpenAPI specification + +To exclude a route and its children from the generated OpenAPI document, use the `Route.hide()` function: + +```kotlin +@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](#assemble-and-serve-the-specification) itself. -This task runs the Kotlin compiler with a custom plugin that analyzes your routing code and produces a -JSON specification. +The OpenAPI and Swagger UI plugins call `.hide()` automatically, so their routes are excluded from the resulting +document. -> Some constructs cannot be evaluated at compile time. The generated specification may be incomplete. Improvements are -> planned for later Ktor releases. +## 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 OpenAPI document manually](#assemble-and-serve-the-specification). +- Use the [OpenAPI](server-openapi.md) or [SwaggerUI](server-swagger-ui.md) plugins to serve the specification and +interactive documentation. + +### 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: + +```kotlin +``` +{src="snippets/openapi-spec-gen/src/main/kotlin/com/example/Application.kt" include-lines="43-46"} + +In this example, the OpenAPI document is serialized using the [`ContentNegotiation`](server-serialization.md) 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. + +> If you want to make serialization explicit or avoid relying on `ContentNegotiation`, you can encode the document +> manually and respond with a JSON: +> +> ```kotlin +> call.respondText( +> Json.encodeToString(docs), +> ContentType.Application.Json +> ) +>``` > {style="note"} -## Serve the specification +### Serve interactive documentation -To make the generated specification available at runtime, you can use the [OpenAPI](server-openapi.md) -or [SwaggerUI](server-swagger-ui.md) plugins. +To expose the OpenAPI specification through an interactive UI, use the [OpenAPI](server-openapi.md) +and [Swagger UI](server-swagger-ui.md) plugins. -The following example serves the generated specification file at an OpenAPI endpoint: +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. ```kotlin -routing { - openAPI("/docs", swaggerFile = "openapi/generated.json") +// 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() + } } -``` \ No newline at end of file +``` + +### 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: + - [Routing structure analysis](#routing-structure-analysis) + - [Code inference](#code-inference) +2. [Comment-based route annotations](#comment-annotations) +3. [Runtime route annotations](#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. \ No newline at end of file diff --git a/topics/server-openapi.md b/topics/server-openapi.md index 77a2be060..6a734bd80 100644 --- a/topics/server-openapi.md +++ b/topics/server-openapi.md @@ -19,9 +19,14 @@ The OpenAPI plugin allows you to generate OpenAPI documentation for your project. -Ktor allows you to generate and serve OpenAPI documentation for your project based on an existing OpenAPI specification. -You can serve an existing YAML or JSON specification, or generate one using the -[OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. +Ktor allows you to serve OpenAPI documentation based on an OpenAPI specification. + +You can provide the OpenAPI specification in one of the following ways: + +* [Serve an existing YAML or JSON file](#static-openapi-file). +* [Generate the specification at runtime using the OpenAPI compiler extension and runtime APIs](#generate-runtime-openapi-metadata). + +In both cases, the OpenAPI plugin assembles the specification on the server and renders the documentation as HTML. ## Add dependencies {id="add_dependencies"} @@ -40,10 +45,13 @@ You can serve an existing YAML or JSON specification, or generate one using the You can replace `$swagger_codegen_version` with the required version of the `swagger-codegen-generators` artifact, for example, `%swagger_codegen_version%`. -## Configure OpenAPI {id="configure-swagger"} +## Use a static OpenAPI file {id="static-openapi-file"} + +To serve OpenAPI documentation from an existing specification, use the [`openAPI()`](%plugin_api_link%) function with +a provided path to the OpenAPI document. -To serve OpenAPI documentation, you need to call the [openAPI](%plugin_api_link%) method that creates a `GET` endpoint with documentation -at the `path` rendered from the OpenAPI specification placed at `swaggerFile`: +The following example creates a `GET` endpoint at the `openapi` path and renders the Swagger UI from the provided +OpenAPI specification file: ```kotlin import io.ktor.server.plugins.openapi.* @@ -54,14 +62,37 @@ routing { } ``` -This method tries to look up the OpenAPI specification in the application resources. -Otherwise, it tries to read the OpenAPI specification from the file system using `java.io.File`. +The plugin first looks for the specification in the application resources. If not found, it attempts to load it from +the file system using `java.io.File`. -By default, the documentation is generated using `StaticHtml2Codegen`. -You can customize generation settings inside the `openAPI` block: +## Generate runtime OpenAPI metadata + +Instead of relying on a static file, you can generate the OpenAPI specification at runtime using metadata produced +by the OpenAPI compiler plugin and route annotations. + +In this mode, the OpenAPI plugin assembles the specification directly from the routing tree: ```kotlin + openAPI(path = "openapi") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing { + routingRoot.descendants() + } +} ``` -{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,56-58,59"} -You can now [run](server-run.md) the application and open the `/openapi` page to see the generated documentation. +With this, you can access the generated OpenAPI documentation at the `/openapi` path, reflecting the current state of the +application. + +> For more information on the OpenAPI compiler extension and runtime APIs, see [](openapi-spec-generation.md). +> +{style="tip"} + +## Configure OpenAPI {id="configure-openapi"} + +By default, documentation is rendered using `StaticHtml2Codegen`. You can customize the renderer inside the `openAPI {}` +block: + +```kotlin +``` +{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,56-58,59"} diff --git a/topics/server-swagger-ui.md b/topics/server-swagger-ui.md index f38fbc75e..11472fe73 100644 --- a/topics/server-swagger-ui.md +++ b/topics/server-swagger-ui.md @@ -19,9 +19,13 @@ The SwaggerUI plugin allows you to generate Swagger UI for your project. -Ktor allows you to generate and serve Swagger UI for your project based on the existing OpenAPI specification. -With Swagger UI, you can visualize and interact with the API resources. You can serve an existing YAML or JSON -specification, or generate one using the [OpenAPI extension](openapi-spec-generation.md) of the Ktor Gradle plugin. +Ktor allows you to generate and serve Swagger UI for your project based on an OpenAPI specification. +With Swagger UI, you can visualize and interact with your API endpoints directly from the browser. + +You can provide the OpenAPI specification in one of the following ways: + +* [Serve an existing YAML or JSON file](#static-openapi-file). +* [Generate the specification at runtime using the OpenAPI compiler extension and runtime APIs](#generate-runtime-openapi-metadata). ## Add dependencies {id="add_dependencies"} @@ -30,10 +34,13 @@ Serving Swagger UI requires adding the `%artifact_name%` artifact in the build s -## Configure Swagger UI {id="configure-swagger"} +## Use a static OpenAPI file {id="static-openapi-file"} -To serve Swagger UI, you need to call the [swaggerUI](%plugin_api_link%) method that creates a `GET` endpoint with Swagger UI -at the `path` rendered from the OpenAPI specification placed at `swaggerFile`: +To serve Swagger UI from an existing OpenAPI specification file, use the [`swaggerUI()`](%plugin_api_link%) function and +specify the file location. + +The following example creates a `GET` endpoint at the `swagger` path and renders the Swagger UI from the provided +OpenAPI specification file: ```kotlin import io.ktor.server.plugins.swagger.* @@ -44,25 +51,46 @@ routing { } ``` -This method tries to look up the OpenAPI specification in the application resources. -Otherwise, it tries to read the OpenAPI specification from the file system using `java.io.File`. +The plugin first looks for the specification in the application resources. If not found, it attempts to load it from +the file system using `java.io.File`. + +## Generate runtime OpenAPI metadata -Optionally, you can customize Swagger UI inside the `swaggerUI` block. -For example, you can use another Swagger UI version or apply a custom style. +Instead of relying on a static file, you can generate the OpenAPI specification at runtime using metadata produced +by the OpenAPI compiler plugin and route annotations: ```kotlin +swaggerUI("/swaggerUI") { + info = OpenApiInfo("My API", "1.0") + source = OpenApiDocSource.Routing(ContentType.Application.Json) { + routingRoot.descendants() + } +} ``` -{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,53-55,59"} -You can now [run](server-run.md) the application and open the `/swagger` page to see the available endpoints, and test them. +With this, you can access the generated OpenAPI documentation at the `/swaggerUI` path, reflecting the current state of +the application. + +> For more information on the OpenAPI compiler extension and runtime APIs, see [](openapi-spec-generation.md). +> +{style="tip"} +## Configure Swagger UI + +You can customize Swagger UI within the `swaggerUI {}` block, for example, by specifying a custom Swagger UI version: + +```kotlin +``` +{src="snippets/json-kotlinx-openapi/src/main/kotlin/com/example/Application.kt" include-lines="40,53-55,59"} ## Configure CORS {id="configure-cors"} -To make sure your API works nicely with Swagger UI, you need to set up a policy for [Cross-Origin Resource Sharing (CORS)](server-cors.md). +To ensure Swagger UI can access your API endpoints correctly, you need to first configure [Cross-Origin Resource Sharing +(CORS)](server-cors.md). + The example below applies the following CORS configuration: -- `anyHost` enables cross-origin requests from any host; -- `allowHeader` allows the `Content-Type` client header used in [content negotiation](server-serialization.md). +* `anyHost` enables cross-origin requests from any host. +* `allowHeader` allows the `Content-Type` client header used for [content negotiation](server-serialization.md). ```kotlin ``` diff --git a/topics/whats-new-330.md b/topics/whats-new-330.md index c23e15d57..90eef8e96 100644 --- a/topics/whats-new-330.md +++ b/topics/whats-new-330.md @@ -176,7 +176,7 @@ This change unifies SSE handling across all client engines and addresses the lim ## Gradle plugin ### OpenAPI specification generation {id="openapi-spec-gen"} - + Ktor 3.3.0 introduces an experimental OpenAPI generation feature via the Gradle plugin and a compiler plugin. This allows you to generate OpenAPI specifications directly from your application code at build time. @@ -189,9 +189,6 @@ It provides the following capabilities: - Security, descriptions, deprecations, and external documentation links - Infer request and response bodies from `call.receive()` and `call.respond()`. - - - #### Generate the OpenAPI specification To generate the OpenAPI specification file from your Ktor routes and KDoc annotations, use the following command: diff --git a/topics/whats-new-340.md b/topics/whats-new-340.md index da5e809f8..106fe6eed 100644 --- a/topics/whats-new-340.md +++ b/topics/whats-new-340.md @@ -151,6 +151,52 @@ routing { } ``` +### Runtime route annotations + + + +Ktor 3.4.0 introduces the `ktor-server-routing-openapi` module, which allows you to attach OpenAPI metadata directly +to routes using runtime annotations. These annotations are applied to routes at runtime and become part of the routing tree, making them available to +OpenAPI-related tooling. + +The API is experimental and requires opting in using `@OptIn(ExperimentalKtorApi::class)`. + +To add metadata to a route at runtime, use the `.describe {}` extension function: + +```kotlin +@OptIn(ExperimentalKtorApi::class) +get("/messages") { + val query = call.parameters["q"]?.let(::parseQuery) + call.respond(messageRepository.getMessages(query)) +}.describe { + parameters { + query("q") { + description = "An encoded query" + required = false + } + } + responses { + HttpStatusCode.OK { + description = "A list of messages" + schema = jsonSchema>() + extension("x-sample-message", testMessage) + } + HttpStatusCode.BadRequest { + description = "Invalid query" + ContentType.Text.Plain() + } + } + summary = "get messages" + description = "Retrieves a list of messages." +} +``` + +You can use this API as a standalone extension or in combination with Ktor's OpenAPI compiler plugin to automatically +generate these calls. The [OpenAPI](server-openapi.md) and +[SwaggerUI](server-swagger-ui.md) plugins also read this metadata when building the OpenAPI specification. + +For more details and examples, see [](openapi-spec-generation.md#runtime-route-annotations). + ### API Key authentication The new [API Key authentication plugin](server-api-key-auth.md) allows you to secure server routes using a shared secret @@ -456,5 +502,58 @@ println("A file saved to ${file.path}") ``` +## Gradle plugin + +### OpenAPI compiler extension + +Previously, the OpenAPI compiler plugin generated a complete, static OpenAPI document at build time. In Ktor 3.4.0, it +instead generates code that provides OpenAPI metadata at runtime, which is consumed by the [OpenAPI](server-openapi.md) +and [Swagger UI](server-swagger-ui.md) plugins when serving the specification. + +The dedicated `buildOpenApi` Gradle task has been removed. The compiler plugin is now automatically applied during +regular builds, and changes to routes or annotations are reflected in the running server without requiring any +additional generation steps. +#### Configuration +Configuration is still done using the `openApi {}` block inside the `ktor` Gradle extension. However, properties used +to define global OpenAPI metadata, such as `title`, `version`, `description`, and `target`, have been deprecated and are +ignored. + +Global OpenAPI metadata is now defined and resolved at runtime rather than during compilation. + +The compiler extension configuration is now limited to feature options that control how metadata is inferred and +collected. + +For users migrating from the experimental preview in Ktor 3.3.0, the configuration has changed as follows: + + + +```kotlin +// build.gradle.kts +ktor { + @OptIn(OpenApiPreview::class) + openApi { + target = project.layout.projectDirectory.file("api.json") + title = "OpenAPI example" + version = "2.1" + summary = "This is a sample API" + } +} +``` + +```kotlin +// build.gradle.kts +ktor { + openApi { + // Global control for the compiler plugin + enabled = true + // Enables and disables inferring details from call handler code + codeInferenceEnabled = true + // Toggles whether all routes should be analysed or only commented ones + onlyCommented = false + } +} +``` + +