papsign / Ktor-OpenAPI-Generator

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

Usage with kotlinx.serialization #42

Open ivan-brko opened 4 years ago

ivan-brko commented 4 years ago

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?

Wicpar commented 4 years ago

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 ?

ivan-brko commented 4 years ago

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.

Wicpar commented 4 years ago

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.

ivan-brko commented 4 years ago

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.

inaiat commented 4 years ago

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!

christiangroth commented 3 years ago

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.

xupyprmv commented 3 years ago

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

xupyprmv commented 3 years ago

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

christiangroth commented 3 years ago

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.