papsign/Ktor-OpenAPI-Generator

Usage with kotlinx.serialization

ivan-brko opened this issue · 9 comments

Hi,
This library looks great!

I am having problems with getting things working in my Ktor application which uses kotlinx.serialization for API serialization/deserialization. I can't find if this library should work (or is tested) with kotlinx.serialization? And if so if there are any specific settings that need to be set?

In principle the Ktor serializer/deserializer should be compatible since it uses Ktror's respond and receive, but i haven't tested.
What doesn't seem to work? Are there errors ?

Yes, I get the following exception:
kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class DataType. For generic classes, such as lists, please provide serializer explicitly.

I'll try to see if I can work around this.

Your issue is related to how kotlinX handles generics. You can get initialisation time types of the generics, see here:
https://github.com/papsign/Ktor-OpenAPI-Generator/blob/master/src/main/kotlin/com/papsign/ktor/openapigen/content/type/ktor/KtorContentProvider.kt

There is currently no way to generate a per route parser/serializer from a single factory IIRC, you would have to manually (through another module) register the parsers/serializers for each specific type. Look at how the KtorContentProvider works, it is a default module loaded through reflection (you can configure the searched packages in the config) with the interface OpenAPIGenModuleExtension.

I'll implement the necessary changes when it is clear what changes are required.

Thanks for the info.

I guessed that I would somehow need to register serializers for everything this library needs to send. Didn't look that much into how kotlinx.serialization works under the hood (and how Ktor calls kotlinx.serialization), I will check that.

Hi,

I created a extension on DataModel to serialize using a kotlinx.serialization.

I few some tests and I got success results.

fun DataModel.kserialize(): JsonElement {
    fun Any?.toJsonElement(): JsonElement {
        return when (this) {
            is Number -> JsonPrimitive(this)
            is String -> JsonPrimitive(this)
            is Boolean -> JsonPrimitive(this)
            is Enum<*> -> JsonPrimitive(this.name)
            is JsonElement -> this
            else -> {
                if (this!=null) System.err.println("The type $this is unknown")
                JsonNull
            }
        }
    }
    fun Map<String, *>.clean(): JsonObject {
        val map = filterValues {
            when (it) {
                is Map<*, *> -> it.isNotEmpty()
                is Collection<*> -> it.isNotEmpty()
                else -> it != null
            }
        }
        return JsonObject(map.mapValues { entry -> entry.value.toJsonElement() }.filterNot { it.value == JsonNull })
    }
    fun cvt(value: Any?): JsonElement? {
        return when (value) {
            is DataModel -> value.kserialize()
            is Map<*, *> -> value.entries.associate { (key, value) -> Pair(key.toString(), cvt(value)) }.clean()
            is Iterable<*> -> JsonArray(value.mapNotNull { cvt(it) })
            else -> value.toJsonElement()
        }
    }
    return this::class.memberProperties.associateBy { it.name }.mapValues { (_, prop) ->
        cvt((prop as KProperty1<DataModel, *>).get(this))
    }.clean()
}

And I need mark @serializable on model classes.

@Serializable
@Response("A String Response")
data class StringResponse(val str: String)

I can use in this way:

routing {
        get("/") {
            call.respondRedirect("/swagger-ui/index.html?url=/openapi.json", true)
        }

        get("/openapi.json") {
            call.respond(openAPIGen.api.kserialize())
        }
    }

Thank you!

I replaced a tiny detail

fun DataModel.kserialize(): JsonElement {
    fun Any?.toJsonElement(): JsonElement {
        return when (this) {
            is Number -> JsonPrimitive(this)
            is String -> JsonPrimitive(this)
            is Boolean -> JsonPrimitive(this)
            is Enum<*> -> JsonPrimitive(this.name)
            is JsonElement -> this
            else -> {
                if (this != null) {
                    // if this happens, then we might have missed to add : DataModel to a custom class so it does not get serialized!
                    throw IllegalStateException("The type ${this.javaClass} ($this) is unknown")
                } else {
                    JsonNull
                }
            }
        }
    }
    ...
}

This leads to errors if you maybe miss to add inheritance from DataModel. Otherwise you have missing values in your openapi.json. Not sure if this is what one wants.

@christiangroth Can you please share the branch where kotlinx serialization is supported?

I was able to figure out what was wrong in my case. Some element serialization simply didn't work since they are not part of DataModel.

So I replaced function fun Any?.toJsonElement(): JsonElement { mentioned in previous posts with next one:

@InternalSerializationApi
private inline fun <reified T> toJsonElement(json: Json, value: T): JsonElement =
  when (value) {
    null -> JsonNull
    is JsonElement -> value
    else -> {
      val serial: KSerializer<T> = value!!::class.serializer() as KSerializer<T>
      json.encodeToJsonElement(serial, value)
    }
  }

where json is kotlinx.serialization.json.Json. After that I was able to generate apidocs that contains exampleRequest and other classes that were marked as @serializable, but were not part of DataModel

Hi @xupyprmv sorry for the later answer and thanks for sharing your solution :) Unfortunately I don't think there is a branch officially supporting kotlinx.serialization, at least I did not found one. But as I am also not actively developing this project I cannot say what the future may bring.