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
.