openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG] [Kotlin] [Kotlin-server] [KTOR] Kotlin server does not generate a REST api

Open Osthekake opened this issue 2 years ago • 3 comments

Bug Report Checklist

  • [ ] Have you provided a full/minimal spec to reproduce the issue?
  • [ ] Have you validated the input using an OpenAPI validator (example)?
  • [x] Have you tested with the latest master to confirm the issue still exists?
  • [x] Have you searched for related issues/PRs?
  • [x] What's the actual output vs expected output?
  • [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description
Paths.kt

When the kotlin-server generator generates ktor code, it creates Resources as described in the ktor documentation, under "Type Safe Routing", seen here in the following format:

    /**
     * Updated user
     * This can only be done by the logged in user.
     * @param username name that need to be deleted 
     * @param body Updated user object 
     */
    @Serializable @Resource("/user/{username}") class updateUser(val username: kotlin.String, val body: User)

One could easily assume that the body parameter here represents the expected body of the request, however all parameters inside a @Resource class are url parameters. It is therefore impossible to trigger this resource, and the generated code makes no sense, even though it compiles and runs properly.

val body: User should not be a part of the params for a @Resource. Only url parameters should be.

In the following example, a part of the url is reflected in the url parameters:

    /**
     * Delete purchase order by ID
     * For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
     * @param orderId ID of the order that needs to be deleted 
     */
    @Serializable @Resource("/store/order/{orderId}") class deleteOrder(val orderId: kotlin.String)

This is also incorrect, according to the doc, as orderId is not a url param. It should be fetched inside the path itself, like this:

    delete<Paths.deleteOrder> {
        val orderId = call.parameters['orderId'] // orderId from url segment
        call.respond(HttpStatusCode.NotImplemented)
    }
UserApi.kt
    get<Paths.getUserByName> {
        val exampleContentType = "application/json"
        val exampleContentString = """{
          "firstName" : "firstName",
          "lastName" : "lastName",
          "password" : "password",
          "userStatus" : 6,
          "phone" : "phone",
          "id" : 0,
          "email" : "email",
          "username" : "username"
        }"""
        
        when (exampleContentType) {
            "application/json" -> call.respond(gson.fromJson(exampleContentString, empty::class.java))
            "application/xml" -> call.respondText(exampleContentString, ContentType.Text.Xml)
            else -> call.respondText(exampleContentString)
        }
    }

This code makes no sense to me. There is a string representing a json. in the first case, we deserialize this string into a generic Gson json, and respond with that, which is then, presumably, immediately serialized again. In the second case, we respond with the same raw string, which is still a json, but we set the content type to xml. I have to assume this is left over from an older variant, but in my opinion it could be removed. I think the generated code should look like this:

    get<Paths.getUserByName> {
        val username = call.parameters["username"]
        val exampleContent = User(
          firstName = "firstName",
          lastName = "lastName",
          password = "password",
          userStatus = 6,
          phone = "phone",
          id = 0,
          email = "email",
          username = "username"
        )
        call.respond(exampleContent)
    }

For completeness, you should get the body from an incoming request like this:

    post<Paths.createUser> {
        val user = call.receive<User>()
        call.respond(HttpStatusCode.NotImplemented)
    }

Note also that @Resources do not need to be serialized, and don't need the @Serializable decorator. They are only used inside the ktor app and don't need to be serialized and sent anywhere.

classpath

The classpath provided in the generated gradle file is the wrong classpath and the jar file does not run.

openapi-generator version

7.1.0

OpenAPI declaration file content or url

This goes for all generation files that use any kind of REST, as far as I can tell, and probably others

Generation Details
$ ./bin/generate-samples.sh bin/configs/kotlin-server-ktor.yaml
Steps to reproduce

Generate and look at the generated files.

Alternatively, to see the classname error, generate and run:

$ java -jar build/libs/kotlin-server.jar
Suggest a fix

See description for what I believe the generated code should look like.

Osthekake avatar Oct 10 '23 08:10 Osthekake

There exists the a variant of the post function, that takes the type of the body as a second type parameter.

So instead of

post<Paths.createUser> { parameters ->
    val user = call.receive<User>()
    call.respond(HttpStatusCode.NotImplemented)
}

one can use

post<Paths.createUser, User> { parameters, user ->
    call.respond(HttpStatusCode.NotImplemented)
}

odzhychko avatar Sep 03 '24 16:09 odzhychko

There exists the a variant of the post function, that takes the type of the body as a second type parameter.

So instead of

post<Paths.createUser> { parameters ->
    val user = call.receive<User>()
    call.respond(HttpStatusCode.NotImplemented)
}

one can use

post<Paths.createUser, User> { parameters, user ->
    call.respond(HttpStatusCode.NotImplemented)
}

That is very handy

Osthekake avatar Oct 07 '24 23:10 Osthekake

@Osthekake @odzhychko I am running into the same problem and I feel like the post function variation does not help

I have this Paths.kt generated by openapi-generator:

object Paths {
    /**
     * Authenticate with login token
     * Validates the authentication token received from the Telegram bot login URL
and returns a JWT access token.

     * @param loginRequest  
     */
    @Serializable @Resource("/auth/login") class login(val loginRequest: LoginRequest)

with

/**
 * 
 *
 * @param token One-time login token from Telegram bot URL
 */
@Serializable

data class LoginRequest (

    /* One-time login token from Telegram bot URL */
    @SerialName(value = "token")
    val token: kotlin.String

)

however, when I implement it:

fun Route.authenticationRoutes(
    authService: AuthenticationService,
    userService: UserService
) {
    post<Paths.login, LoginRequest> { parameters, loginRequest ->
         ....

I will always receive a HTTP 400 response when I call http://localhost:8080/api/v1/auth/login. However when I call http://localhost:8080/api/v1/auth/login?token=abc the call goes through and I even get the token as in the json body of the request. So it seems to me that this is indeed a bug? Or where am I taking the wrong turn?

Tyde avatar Dec 08 '25 16:12 Tyde