If someone can, please explain why this TypeConverter works:
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
abstract class SetConverter <T>{
private val gson = Gson()
private val setType = object: TypeToken<Set<T>>(){}.type
@TypeConverter
fun toJson(set: Set<T>): String{
return gson.toJson(set, setType)
}
@TypeConverter
fun fromJson(json: String): Set<T>{
return gson.fromJson(json, setType)
}
}
In Entity I use SetConverter through this implementation:
class CalendarSetConverter: SetConverter<Calendar>()
And this works.
But a similar TypeConverter for the class does not work.
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
abstract class ClassConverter<T> {
private val gson = Gson()
private val type = object : TypeToken<T>(){}.type
@TypeConverter
fun toJson(value: T): String{
return gson.toJson(value, type)
}
@TypeConverter
open fun fromJson(json: String): T{
return gson.fromJson(json, type)
}
}
Implementation in Entity:
class CalendarConverter : ClassConverter<Calendar>()
And this implementation throw error in runtime while read Calendar from database:
Caused by: java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to java.util.Calendar
How to create universal generic TypeConverter for Room?
Actually your CalendarSetConverter
is most likely also not behaving as you intended, but due to type erasure you most likely have not noticed the issue yet. If you try to use the elements inside the Set
returned by CalendarSetConverter
you will also run into a ClassCastException
:
val set = CalendarSetConverter().fromJson(json);
// ClassCastException: LinkedTreeMap cannot be cast to class Calendar
val calendar: Calendar = set.first()
The underlying issue for both of this is also type erasure. You should never create an object : TypeToken<...>
which captures a type parameter (e.g. T
) unless that type parameter is reified. Otherwise at compile time the upper bound of the type parameter is used, or Any
if no explicit bound is specified.
So what your code actually looks like is this:
val setType = object : TypeToken<Set<Any>>(){}.type
val type = object : TypeToken<Any>(){}.type
And Gson deserializes Any
(respectively java.lang.Object
) as a Map
, more precisely as its internal class LinkedTreeMap
, which causes the ClassCastException
you are experiencing. This does not apply to serialization (toJson
) where Gson will use the runtime type when serializing Any
.
To fix this you will have to take the TypeToken
as constructor parameter, e.g.:
abstract class ClassConverter<T>(
val typeToken: TypeToken<T>
) {
private val gson = Gson()
@TypeConverter
fun toJson(value: T): String {
return gson.toJson(value, typeToken.type)
}
@TypeConverter
fun fromJson(json: String): T {
// Note: This uses the fromJson(String, TypeToken) overload added in Gson 2.10
return gson.fromJson(json, typeToken)
}
}
And then create the converter subclasses like this:
class CalendarConverter : ClassConverter<Calendar>(TypeToken.get(Calendar::class.java))
class CalendarSetConverter : ClassConverter<Set<Calendar>>(object : TypeToken<Set<Calendar>>() {})
I don't know however if / how well this works with Room. Maybe this question provides better solutions to this.