androidgenericsgsonandroid-roomtypeconverter

Room generic TypeConverter


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?


Solution

  • 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.