Ktor: Serialize/Deserialize JSON with List as root in Multiplatform

Update with ktor 1.3.0:

Now you're able to receive default collections(such a list) from the client directly:

@Serializable
data class User(val id: Int)

val response: List<User> = client.get(...)
// or client.get<List<User>>(...)

Before ktor 1.3.0:

There is no way to (de)serialize such JSON in the kotlinx.serialization yet.

For serialization you could try something like this:

fun serializer(data: Any) = if (data is List<*>) {
   if (data is EmptyList) String::class.serializer().list // any class with serializer 
   else data.first()::class.serializer().list
} else data.serializer()

And there are no known ways to get the list deserializer.


You can write wrapper and custom serializer:

@Serializable
class MyClassList(
    val items: List<MyClass>
) {

    @Serializer(MyClassList::class)
    companion object : KSerializer<MyClassList> {

        override val descriptor = StringDescriptor.withName("MyClassList")

        override fun serialize(output: Encoder, obj: MyClassList) {
            MyClass.serializer().list.serialize(output, obj.items)
        }

        override fun deserialize(input: Decoder): MyClassList {
            return MyClassList(MyClass.serializer().list.deserialize(input))
        }
    }
}

Register it:

HttpClient {
    install(JsonFeature) {
        serializer = KotlinxSerializer().apply {
            setMapper(MyClassList::class, MyClassList.serializer())
        }
    }
}

And use:

suspend fun fetchItems(): List<MyClass> {
    return client.get<MyClassList>(URL).items
}

This is more of a workaround but after stepping through KotlinxSerializer code I couldn't see any other way round it. If you look at KotlinxSerializer.read() for example you can see it tries to look up a mapper based on type but in this case it's just a kotlin.collections.List and doesn't resolve. I had tried calling something like setListMapper(MyClass::class, MyClass.serializer()) but this only works for serialization (using by lookupSerializerByData method in write)

override suspend fun read(type: TypeInfo, response: HttpResponse): Any {
    val mapper = lookupSerializerByType(type.type)
    val text = response.readText()

    @Suppress("UNCHECKED_CAST")
    return json.parse(mapper as KSerializer<Any>, text)
}

So, what I ended up doing was something like (note the serializer().list call)

suspend fun fetchBusStops(): List<BusStop> {
    val jsonArrayString = client.get<String> {
        url("$baseUrl/stops.json")
    }

    return JSON.nonstrict.parse(BusStop.serializer().list, jsonArrayString)
}

Not ideal and obviously doesn't make use of JsonFeature.