papsign / Ktor-OpenAPI-Generator

Ktor OpenAPI/Swagger 3 Generator
Apache License 2.0
241 stars 42 forks source link

Deserialisation of List<UUID> in body fails during runtime #112

Open LukasForst opened 2 years ago

LukasForst commented 2 years ago

post fails to deserialise List<UUID> in body, even though Swagger knows that the correct type is UUID. Kotlin says it's UUID, however during runtime, it's String, which results in ClassCastException when accessing the element. My project is using latest Jackson.

Minimal working example:

fun NormalOpenAPIRoute.testRoute() {
    route("/test").post<Unit, String, List<UUID>> { _, listUuids ->
        println(listUuids.joinToString(separator = " - ") { it.toString() }) // access on the element throws java.lang.ClassCastException
        respond("OK")
    }
}

Request:

curl -X POST "http://localhost:8080/test" -H "accept: application/json" -H "Content-Type: application/json" -d "[\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"]"

Debugger: image

Exception:

java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.UUID (java.lang.String and java.util.UUID are in module java.base of loader 'bootstrap')
    at com.wire.troy.routing.ConversationRoutesKt$testRoute$1$1.invoke(ConversationRoutes.kt:81)
    at kotlin.text.StringsKt__AppendableKt.appendElement(Appendable.kt:85)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinTo(_Collections.kt:3344)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinToString(_Collections.kt:3361)
    at kotlin.collections.CollectionsKt___CollectionsKt.joinToString$default(_Collections.kt:3360)

And the openapi.json

{
  "/test": {
    "post": {
      "requestBody": {
        "content": {
          "application/json": {
            "schema": {
              "items": {
                "format": "uuid",
                "nullable": false,
                "type": "string"
              },
              "nullable": false,
              "type": "array"
            }
          }
        }
      },
      "responses": {
        "200": {
          "content": {
            "application/json": {
              "schema": {
                "nullable": false,
                "type": "string"
              }
            }
          },
          "description": "OK"
        }
      }
    }
  }
}
kkalisz commented 2 years ago

To be honest this is not an open API generator issue. It is mostly related to generic types information erasure at runtime. Jackson can't get generic type information so it is not able to propper parse this type. This will not work for top-level generic objects. But it should be fine for such a case. class UuidsList(val values: List<UUID>

But this will require changing payload from list of uuids, to object that contains a list of UUIDs

Another solution will be to use post "x-www-form-urlencoded" content type with key-value pairs. But this will look exactly the same as the previous example (UuidsList)

We can use inheritance to preserve generic type at runtime, But unfortunately, this will not work out of the box as code from DefaultCollectionSchemaProvider needs to be adjusted as for now it only handles current class generic arguments without supertype support.

class UuidsList: ArrayList<UUID> { }

I have created some dirty working solution https://github.com/kkalisz/Ktor-OpenAPI-Generator/tree/generic_parameters https://github.com/kkalisz/Ktor-OpenAPI-Generator/blob/generic_parameters/src/test/kotlin/com/papsign/ktor/openapigen/TopLevelGenericObjectRequestTest.kt