https://kotlinlang.org logo
Title
a

addamsson

07/20/2020, 2:34 PM
Can you point me to some good examples and/or clear documentation on how to deserialize parameters when using Ktor's
ContentNegotiation
feature? I've been banging my head against the wall for ours and it is still not working for me. I have parameter classes with the
@Searializable
annotation:
@Serializable
data class ShowPostURL(
        val date: String,
        val titleSlug: String,
        override val redirectTo: String? = null
)
and no matter what I do
call.receive()
won't work. I'm getting
HTTP 415
errors and Ktor doesn't even log anything. I've added the serialization support as well:
install(ContentNegotiation) {
    json
}
How do I fix this? This is how I'm trying to use it:
accept(ContentType.Any) {
    get(operation.route) {
        val input = call.receive(ShowPostURL::class)
        call.respondText("foo")
    }
}
a

addamsson

07/20/2020, 2:36 PM
I've already been there, it doesn't help
and what you linked says that it is deprecated (the
serialization()
part)
and the docs don't say how to use this with
receive
I'm supposed to see results for basically anything if I use
ContentType.Any
this class is not received as json though
i'm trying to deserialize form the path like this
/foo/{date}/{titleSlug}?redirectTo=bar
e

e5l

07/20/2020, 2:39 PM
install(ContentNegotiation) {
        json()
    }
a

addamsson

07/20/2020, 2:39 PM
is this even possible with
receive()
e

e5l

07/20/2020, 2:39 PM
Could you try adding
()
after json?
a

addamsson

07/20/2020, 2:39 PM
i tried, it says it doesn't have an invoke method
but i'm not trying to deserialize form
json
but form path / query parameters
e

e5l

07/20/2020, 2:40 PM
Strange, it works for me. Could you share the imports you're using?
a

addamsson

07/20/2020, 2:40 PM
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.request.receive
import io.ktor.response.respondText
import io.ktor.routing.Routing
import io.ktor.routing.accept
import io.ktor.routing.get
e

e5l

07/20/2020, 2:41 PM
import io.ktor.serialization.*
could you add that one?
a

addamsson

07/20/2020, 2:42 PM
i added it but intellij says it is unused
and i get 415 😞
e

e5l

07/20/2020, 2:49 PM
I'm lost. Could you file an issue with snippet? Let's figure out what's happening
a

addamsson

07/20/2020, 2:54 PM
this is a MWE
import io.ktor.application.Application
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.ContentNegotiation
import io.ktor.http.ContentType
import io.ktor.request.receive
import io.ktor.response.respondText
import io.ktor.routing.accept
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.serialization.json
import io.ktor.server.engine.applicationEngineEnvironment
import io.ktor.server.engine.connector
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.serialization.Serializable

fun main(args: Array<String>) {
    val env = applicationEngineEnvironment {
        module {
            example()
        }
        connector {
            port = 8080
            host = "0.0.0.0"
        }
    }
    embeddedServer(Netty, env).apply {
        start(wait = true)
    }
}

@Serializable
data class PostURL(
        val date: String,
        val titleSlug: String,
        val redirectTo: String? = null
)

fun Application.example() {

    install(ContentNegotiation) {
        json()
    }

    routing {
        accept(ContentType.Any) {
            get("/foo/{date}/{titleSlug}") {
                val input = call.receive(PostURL::class)
                println("$input was received.")
                call.respondText("foo")
            }
        }
    }
}
run it and call it with
<http://localhost:8080/foo/2018-10-21/wtf-is-this.html>
you'll get 415
@e5l I put this in a
try/catch
and this is what i'm getting:
io.ktor.features.UnsupportedMediaTypeException: Content type */* is not supported
	at io.ktor.features.ContentNegotiation$Feature$install$3.invokeSuspend(ContentNegotiation.kt:167)
	at io.ktor.features.ContentNegotiation$Feature$install$3.invoke(ContentNegotiation.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:183)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:27)
	at io.ktor.request.ApplicationReceiveFunctionsKt.receive(ApplicationReceiveFunctions.kt:110)
	at io.ktor.request.ApplicationReceiveFunctionsKt.receive(ApplicationReceiveFunctions.kt:90)
	at org.agorahq.agora.delivery.ExampleKt$example$2$2$1.invokeSuspend(Example.kt:54)
	at org.agorahq.agora.delivery.ExampleKt$example$2$2$1.invoke(Example.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:183)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:27)
	at io.ktor.routing.Routing.executeResult(Routing.kt:147)
	at io.ktor.routing.Routing.interceptor(Routing.kt:34)
	at io.ktor.routing.Routing$Feature$install$1.invokeSuspend(Routing.kt:99)
	at io.ktor.routing.Routing$Feature$install$1.invoke(Routing.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
	at io.ktor.features.ContentNegotiation$Feature$install$1.invokeSuspend(ContentNegotiation.kt:107)
	at io.ktor.features.ContentNegotiation$Feature$install$1.invoke(ContentNegotiation.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:183)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:27)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$2.invokeSuspend(DefaultEnginePipeline.kt:120)
	at io.ktor.server.engine.DefaultEnginePipelineKt$defaultEnginePipeline$2.invoke(DefaultEnginePipeline.kt)
	at io.ktor.util.pipeline.SuspendFunctionGun.loop(PipelineContext.kt:318)
	at io.ktor.util.pipeline.SuspendFunctionGun.proceed(PipelineContext.kt:163)
	at io.ktor.util.pipeline.SuspendFunctionGun.execute(PipelineContext.kt:183)
	at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:27)
	at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invokeSuspend(NettyApplicationCallHandler.kt:40)
	at io.ktor.server.netty.NettyApplicationCallHandler$handleRequest$1.invoke(NettyApplicationCallHandler.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startCoroutineUndispatched(Undispatched.kt:55)
	at kotlinx.coroutines.CoroutineStart.invoke(CoroutineStart.kt:111)
	at kotlinx.coroutines.AbstractCoroutine.start(AbstractCoroutine.kt:158)
	at kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:54)
	at kotlinx.coroutines.BuildersKt.launch(Unknown Source)
	at io.ktor.server.netty.NettyApplicationCallHandler.handleRequest(NettyApplicationCallHandler.kt:30)
	at io.ktor.server.netty.NettyApplicationCallHandler.channelRead(NettyApplicationCallHandler.kt:24)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:377)
	at io.netty.channel.AbstractChannelHandlerContext.access$600(AbstractChannelHandlerContext.java:59)
	at io.netty.channel.AbstractChannelHandlerContext$7.run(AbstractChannelHandlerContext.java:368)
	at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
	at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:748)
it seems that i need to add support for this somehow
but it is not in the docs how to do that
e

e5l

07/20/2020, 3:04 PM
It looks like we don't support patterns in matcher
a

addamsson

07/20/2020, 3:06 PM
is there no way to deserialize an url into a class? 😮
e

e5l

07/20/2020, 3:07 PM
You can try using
Location
for that
a

addamsson

07/20/2020, 3:07 PM
is it stable enough?
a

addamsson

07/20/2020, 3:07 PM
the docs say it is an experimental api
apart from that I don't want to pollute my business logic with Ktor annotations
that's one of the primary reasons why i'm not using Spring
e

e5l

07/20/2020, 3:08 PM
Yep, but we're not going to break it
a

addamsson

07/20/2020, 3:08 PM
is there an alternative?
e

e5l

07/20/2020, 3:08 PM
Yep, fair point
a

addamsson

07/20/2020, 3:08 PM
my business modules don't know about Ktor 😞
e

e5l

07/20/2020, 3:08 PM
Let me thing a bit
a

addamsson

07/20/2020, 3:09 PM
thanks 👍
n

nschulzke

07/20/2020, 3:10 PM
I ran into this same problem (including not wanting to risk using the experimental Location API). I ended up writing a short function to deserialize the parameters.
a

addamsson

07/20/2020, 3:12 PM
what's the difference between
call.parameters
and
call.receiveParameters()
? It is not clear from the docs
n

nschulzke

07/20/2020, 3:13 PM
receiveParameters
gets the request body as a
Parameters
object.
call.parameters
is what comes in in the URL. Not great naming.
👍 2
a

addamsson

07/20/2020, 3:14 PM
oh so
receiveParameters()
is the alternative for
receive()
if you're not doing ContentNegotiation?
thx
n

nschulzke

07/20/2020, 3:15 PM
In practice, it seems to deserialize form data as sent by HTML forms.
So, yeah.
a

addamsson

07/20/2020, 3:17 PM
can i use
ContentNegotiation
with forms?
call.parameters
seems to include query parameters as well
n

nschulzke

07/20/2020, 3:17 PM
Yes, it does.
A year ago I couldn't get ContentNegotiation to work with forms.
a

addamsson

07/20/2020, 3:17 PM
this is a bummer
i'm using forms as a fallback method if javascript doesn't work for some reason 😞
n

nschulzke

07/20/2020, 3:18 PM
I ended up using an extension function on the StringValues class to do both form deserialization and url parameters serialization.
Since
receiveParameters
and
parameters
have the same type, you can deserialize both the same way, once you have a way to do one.
a

addamsson

07/20/2020, 3:19 PM
makes sense
thanks
👍 1
@e5l do you have plans for fixing this?
e

e5l

07/20/2020, 3:20 PM
Yep, I filed an issue about the names. And wrote notes about forms with
ContentNegotations
compatibility
Great thanks for the investigation!
a

addamsson

07/20/2020, 3:22 PM
thanks, @e5l
i'm gonna write a
receiveAs()
method for the time being on the
Parameters
class
so is there something I can do if I want to enable receiving data structures as json and as form data as well?
what i'm trying ot do is to have regular forms
which are enhanced if javascript is present
to API calls
n

nschulzke

07/20/2020, 3:26 PM
Yeah, I was doing something similar. I check the content-type. If it's application/json, I deserialize with the normal
receive
method, if it's not I deserialize with
receiveParameters
and my extension.
a

addamsson

07/20/2020, 3:26 PM
but right now i can't use the same receiving logic for both
n

nschulzke

07/20/2020, 3:27 PM
All that is behind an extension function on
PipelineContext<Unit, ApplicationCall>
, so you don't have to have the checks everywhere.
a

addamsson

07/20/2020, 3:27 PM
what do you mean?
can you post an example?
n

nschulzke

07/20/2020, 3:30 PM
Not the actual code, but close:
PipelineContext<Unit, ApplicationCall>.myReceive(type: KClass<T>) {
    if (request.contentType().match(ContentType.Application.Json) {
        receive(type);
    } else {
        receiveParameters().myDeserialize(type);
    }
}
So, if it's JSON, the extension acts as though it's just the
receive
method. Otherwise, it uses the custom deserialization.
It doesn't save any overhead on the checks, but it means you don't have to think about them when actually writing route code.
https://ktor.io/servers/features/content-negotiation.html It looks like you could also write your own ContentConverter that handles it for you, and register it. I'm not sure why I didn't find this last year (maybe the docs on this are new), but this actually looks like a better way if it works.
So, form data may not be natively supported, but once you have a way to deserialize you could use the native
receive
method and just register a new
ContentConverter
.
a

addamsson

07/20/2020, 6:22 PM
thanks, this is great!