Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage with kotlinx.serialization #42

Open
ivan-brko opened this issue May 7, 2020 · 9 comments
Open

Usage with kotlinx.serialization #42

ivan-brko opened this issue May 7, 2020 · 9 comments

Comments

@ivan-brko
Copy link

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
Copy link
Collaborator

Wicpar commented May 7, 2020

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
Copy link
Author

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
Copy link
Collaborator

Wicpar commented May 8, 2020

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
Copy link
Author

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
Copy link

inaiat commented Jul 26, 2020

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
Copy link

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
Copy link

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

@xupyprmv
Copy link

xupyprmv commented Jun 24, 2021

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
Copy link

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.

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

No branches or pull requests

5 participants