Consider the following deserialization in Ktor setup context:
@Serializable
data class TestingDTO(val texts: List<String>)
fun main() {
embeddedServer(
Netty,
port = 8080,
host = "0.0.0.0",
module = Application::myApp
).start(wait = true)
}
fun Application.myApp() {
install(ContentNegotiation) {
json()
}
routing {
post("/testing") {
runCatching {
val receivedTestingDTO = call.receive<TestingDTO>()
println(receivedTestingDTO)
call.respond(HttpStatusCode.Created)
}.onFailure {
call.respond(HttpStatusCode.BadRequest, it.message ?: "deserialization problem!!!")
}
}
}
}
curl -v -d '{"texts": ["foo", "bar", 1]}' -H 'Content-Type: application/json' http://127.0.0.1:8080/testing
Note: notice the JSON array containing strings and number at same time.
Now, if we run that, basically the call.receive<TestingDTO>()
deserilizes the JSON of the request to the TestingDTO
type "perfectly", even we puting a number inside that JSON array, causing the transformation of all items from that to String
(in order to fit them in the target texts
list?).
However if we use as deserialization method other kotlin API, the class kotlinx.serialization.json.Json
, the example request fails as expected. For example, the runCatching
block:
runCatching {
// now consuming request to read raw json as "text"
// and using kotlinx class to deserialize that text fails
// if in request exists a diferent type that doesn't fits the target
// TestingDTO List
val receivedTestingDTO = Json.decodeFromString<TestingDTO>(call.receiveText())
receivedTestingDTO.texts.forEach {
println("the value [$it] is instance of ${it::class}")
}
call.respond(HttpStatusCode.Created)
}.onFailure {
call.respond(HttpStatusCode.BadRequest, it.message ?: "deserialization problem!!!")
}
}
So, what exactly is happening here? Why those two different behaviours? Am I missing something about the call.receive<>()
function specification?
If not, the "working" approach is worth of using?
This behavior is the result of the isLenient json configuration builder.
In Ktor the default Json configuration used for all request uppon installing ContentNegotiation
is:
public val DefaultJson: Json = Json {
encodeDefaults = true
isLenient = true
allowSpecialFloatingPointValues = true
allowStructuredMapKeys = true
prettyPrint = false
useArrayPolymorphism = false
}
In your example you use Json.decodeFromString
without configuration. By default, isLenient
is false.
You should do something like this
val codec = Json {
isLenient = true
}
val receivedTestingDTO = codec.decodeFromString<TestingDTO>(call.receiveText())
to have the same behavior as Ktor.
Note:
You can force one json configuration by providing your own builder in the json function.
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
}