Skip to content

Kotlin ktor delegate pattern support#23756

Open
chomnoue wants to merge 11 commits into
OpenAPITools:masterfrom
chomnoue:kotlin-ktor-delegate-pattern-support
Open

Kotlin ktor delegate pattern support#23756
chomnoue wants to merge 11 commits into
OpenAPITools:masterfrom
chomnoue:kotlin-ktor-delegate-pattern-support

Conversation

@chomnoue
Copy link
Copy Markdown

@chomnoue chomnoue commented May 11, 2026

Fixes #23755
Adds delegate pattern for the kotlin-server ktor generator.
Integration tests added under samples/server/petstore/kotlin-server/ktor-delegate-pattern
See the readme under samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/README.md for usage example.

Using the delegate pattern (ktor)

When generating a server with the delegatePattern enabled, the router delegates request handling to small, focused interfaces you implement. This is ideal for API‑first development: you wire your business logic without touching generated routing code.

What gets generated

  • For each API: an interface org.openapitools.server.apis.<ApiName>Delegate with one suspend function per operation. Each function receives parsed parameters plus a io.ktor.server.routing.RoutingCall named call.

  • In org.openapitools.server.infrastructure:

    • AppDelegates – a data holder aggregating all optional delegates.
    • Delegates – helpers to register and inject delegates (Application.delegates(AppDelegates(...)), RoutingCall.delegates(AppDelegates(...)).
    • APINotImplementedException – thrown when a delegate is missing.
      BadParameterException – used for parameter validation errors (400).
  • Generated interface: org.openapitools.server.apis.PetApiDelegate

  • Generated interface: org.openapitools.server.apis.StoreApiDelegate

  • Generated interface: org.openapitools.server.apis.UserApiDelegate

Implement a delegate

Implement the generated interface(s) in your codebase. Example (using the first generated API):

package com.example.impl

import org.openapitools.server.apis.PetApiDelegate
import io.ktor.server.routing.RoutingCall
import 

class PetApiDelegateImpl : PetApiDelegate {

    override suspend fun addPet(pet: Pet, call: RoutingCall): Pet {
        // Access authentication, request, etc. via the call if needed
        // val principal = call.principal<org.openapitools.server.infrastructure.ApiPrincipal>()
        // Return the payload expected by the spec (or Unit if no body)
        TODO("Provide implementation")
    }

}

Notes:

  • Do not call call.respond(...) inside the delegate. Return the value; the generated route will serialize and respond for you. For operations without a response body, return kotlin.Unit and the route responds with 200 by default.

Register your delegates

Register your implementations once during application startup so routes can resolve them:

import org.openapitools.server.infrastructure.AppDelegates
import org.openapitools.server.infrastructure.delegates
import org.openapitools.server.AllApis
import io.ktor.server.application.Application
import io.ktor.server.routing.routing

fun Application.module() {
    delegates(
        AppDelegates(
            // Property names are lowerCamelCase of the API class + "Delegate"
            // Provide only what you implement; others can be left null
            // petApiDelegate = PetApiDelegateImpl(),
            // storeApiDelegate = StoreApiDelegateImpl(),
            // userApiDelegate = UserApiDelegateImpl(),

        )
    )

    // ... install features, etc.
    routing {
        AllApis()
    }
}

If you prefer to register APIs individually:

import org.openapitools.server.apis.PetApi
import org.openapitools.server.apis.StoreApi
import org.openapitools.server.apis.UserApi

import io.ktor.server.application.Application
import io.ktor.server.routing.routing

fun Application.module() {
    // ...
    routing {
        PetApi()
        StoreApi()
        UserApi()

    }
}

If you prefer per-request scoping, you may also attach delegates to a specific RoutingCall:

call.delegates(AppDelegates(petApiDelegate = PetApiDelegateImpl()))

Runtime behavior

  • If a route is invoked and its corresponding delegate is not provided, the server throws APINotImplementedException and responds with 501 Not Implemented.
  • If a required parameter is missing or invalid, BadParameterException is thrown and the server responds with 400 Bad Request.
  • You can access authenticated principals within delegates via call.principal<org.openapitools.server.infrastructure.ApiPrincipal>() when auth is enabled for the operation.

Handle delegate exceptions with StatusPages

To return consistent responses for delegate errors, install Ktor StatusPages and map both exceptions:

import io.ktor.http.HttpStatusCode
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.plugins.statuspages.exception
import io.ktor.server.response.respondText
import org.openapitools.server.infrastructure.APINotImplementedException
import org.openapitools.server.infrastructure.BadParameterException

install(StatusPages) {
    exception<Throwable> { call, cause ->
        when (cause) {
            is BadParameterException -> {
                call.respondText(
                    status = cause.statusCode,
                    text = mapOf(
                        "message" to (cause.message ?: "Invalid parameter"),
                        "parameter" to cause.parameterName,
                        "validation" to cause.validationType,
                        "expected" to cause.expectedValue,
                        "actual" to cause.actualValue
                    ).filterValues { it != null }.mapValues { it.toString() }.toString()
                )
            }

            is APINotImplementedException -> {
                call.respondText(
                    status = HttpStatusCode.NotImplemented,
                    text = cause.message ?: "API not implemented"
                )
            }
        }
    }
}

Handle file uploads in delegates

For multipart endpoints, generated delegates receive an <operation_id>MultiPartReceiver (operation-specific name) so you can parse form fields and process file parts inside your implementation.

Example (uploadFile):

import io.ktor.server.routing.RoutingCall
import io.ktor.utils.io.toByteArray
import org.openapitools.server.apis.PetApiDelegate
import org.openapitools.server.models.ModelApiResponse

class PetApiDelegateImpl : PetApiDelegate {
    override suspend fun uploadFile(
        petId: Long,
        uploadFileMultipartReceiver: PetApiDelegate.UploadFileMultiPartReceiver,
        call: RoutingCall
    ): ModelApiResponse {
        var fileName: String? = null
        var fileContent: String? = null

        val multipart = uploadFileMultipartReceiver.receiveMultipart {
            onReceiveFile {
                fileName = originalFileName
                fileContent = provider().toByteArray().decodeToString()
            }
        }

        val additionalMetadata = multipart.additionalMetadata

        return ModelApiResponse(
            code = 200,
            message = "uploaded file=$fileName metadata=$additionalMetadata content=$fileContent"
        )
    }
}

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work. Missing information here may result in delayed response from the community.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • File the PR against the correct branch: master (upcoming 7.x.0 minor release - breaking changes with fallbacks), 8.0.x (breaking changes without fallbacks)
  • If your PR solves a reported issue, reference it using GitHub's linking syntax (e.g., having "fixes #123" present in the PR description)
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@karismann, @Zomzog, @andrewemery @4brunu @yutaka0m @stefankoppier @e5l @dennisameling


Summary by cubic

Adds delegate pattern support to the kotlin-server generator for ktor, letting routes call generated *ApiDelegate interfaces for clean separation between transport and business logic. Also fixes multipleOf validation for floating-point values and improves enum/number parameter parsing with safer delegate injection.

  • New Features

    • New delegatePattern option for ktor in kotlin-server (documented in generator options).
    • Generates *ApiDelegate interfaces per API and an AllApis() route helper.
    • Infrastructure: AppDelegates, Delegates (safe as? lookups), APINotImplementedException, BadParameterException.
    • Parameter validation plus model validate() methods; violations return 400 via BadParameterException.
    • Validation: multipleOf now correctly supports float/double values.
    • Parameter parsing: enums via fromValue(), numbers via to<Type>(); invalids produce clear 400 errors.
    • Multipart support via operation-scoped *MultiPartReceiver.
    • New samples/server/petstore/kotlin-server/ktor-delegate-pattern with tests and a Maven profile.
  • Migration

    • Enable with additionalProperties.delegatePattern=true when generating kotlin-server with ktor.
    • Implement generated XxxApiDelegate and register once: Application.delegates(AppDelegates(xxxApiDelegate = ...)) (or per-request via call.delegates(...)).
    • Return values from delegates; do not call call.respond(...). Unimplemented ops throw APINotImplementedException (501).
    • Install Ktor StatusPages to handle BadParameterException (400) and APINotImplementedException (501).

Written for commit 3de9af8. Summary will update on new commits.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

14 issues found across 79 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml">

<violation number="1" location="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/main/resources/logback.xml:4">
P2: Use calendar year (`yyyy`) instead of week-year (`YYYY`) in the Logback timestamp pattern.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/Delegates.kt.mustache:44">
P1: Unsafe `as T` casting makes missing delegates crash before the intended APINotImplementedException path can run.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/README.mustache:155">
P2: Catch-all StatusPages handler has no fallback, so unexpected throwables are intercepted but not responded to.</violation>
</file>

<file name="docs/generators/kotlin-server.md">

<violation number="1" location="docs/generators/kotlin-server.md:25">
P3: The documented default for `delegatePattern` is wrong: the generator defaults it to `false`, so this line misleads users about the default scaffold shape.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/_common_validation.mustache:57">
P2: Floating-point multipleOf checks use exact modulo equality, which can falsely reject valid Float/Double values due to precision error.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java:182">
P1: delegatePattern is registered with the wrong default value source (`getAutoHeadFeatureEnabled()`), which can make the option default to the auto-head setting instead of its own false default.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache:17">
P2: Multipart file-only operations can generate an illegal `data class XMultiPart()` with no constructor properties, causing Kotlin compilation to fail.</violation>

<violation number="2" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache:39">
P1: Generated code throws the wrong exception type/signature here; it should use `BadParameterException`, otherwise the template emits uncompilable code for required multipart parameters.</violation>

<violation number="3" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/apiDelegate.mustache:46">
P1: Generated multipart builder passes nullable builder properties into non-null constructor parameters, causing compile errors for non-null form fields.</violation>
</file>

<file name="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache:1">
P1: Enum request parsing uses Kotlin constant names instead of the generated enum wire values, so valid OpenAPI enum inputs can be rejected when names differ from serialized values.</violation>

<violation number="2" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache:1">
P1: Numeric parameters are parsed with `{{{dataType}}}(it)`, which can generate invalid Kotlin for common numeric types like Int/Long/Float/Double.</violation>

<violation number="3" location="modules/openapi-generator/src/main/resources/kotlin-server/libraries/ktor/_extract_param_value.mustache:1">
P1: Uses `BadRequestException` with `parameterName`/`validationType`, but the Ktor delegate templates and documented handler path use `BadParameterException`; this likely generates invalid or unhandled validation exceptions.</violation>
</file>

<file name="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt">

<violation number="1" location="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/TestUtil.kt:43">
P2: Catch-all StatusPages handler swallows unexpected exceptions because it matches every `Throwable` but only handles two specific types and has no fallback path.</violation>
</file>

<file name="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt">

<violation number="1" location="samples/server/petstore/kotlin-server/ktor-delegate-pattern/src/test/kotlin/org/openapitools/server/PetApiTest.kt:246">
P2: Invalid-status test doesn't exercise validation; it only asserts the no-delegate 501 fallback, so it can pass even if status validation is broken.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Comment thread docs/generators/kotlin-server.md Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ] Feature Request Description

1 participant