jsonkotlinserializationktorkotlinx.serialization

Why is this call.receive<T>() deserializing json array items entirely to string type in Ktor?


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?


Solution

  • 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
        })
    }