Search code examples
kotlinandroid-roomtypeconverter

Room TypeConverter for a List of custom Object fails


I have a list of custom objects and the TypeConverters are not working as I expected.

Method threw 'com.google.gson.JsonSyntaxException' exception.

java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2 path $

@Query("SELECT emaps FROM emaps_users WHERE userId = :userId LIMIT 1")
    suspend fun getUserEmapsIds(userId : Int) : List<Emap>

Thats the query thats throwing the error.

Here my class:

ENTITY

@Entity(tableName = "emaps_users")
data class UserEmaps(
    @PrimaryKey(autoGenerate = false)
    val userId : Int,

    @field:TypeConverters(ListEmapConverter::class)
    val emaps : List<Emap>
)

@Entity(tableName = "eMaps")
data class Emap(
    @PrimaryKey(autoGenerate = false)
    override val id : Int = -1,
    var nombreEMap : String = "",
    var estado : Estado? = Estado.NONE,
    private var listPois : List<Int> = mutableListOf(),
    var isla: Isla? = Isla.NONE,
    var publico : MutableSet<Publico> = mutableSetOf(),
    var tematica : MutableSet<Tematica> = mutableSetOf(),
    var dificultad : MutableSet<Dificultad> = mutableSetOf(),
    var duracion : MutableSet<Duracion> = mutableSetOf(),
    var descripcion : String = "",
    var isCustom : Boolean = true,
    var isPublic : Boolean = false
)

TYPECONVERTERS

@TypeConverter
    fun fromEmapListToString(emaps: List<Emap>): String {
        return GsonBuilder().create().toJson(emaps)
    }

    @TypeConverter
    fun fromStringtoEmapList(emapsString: String): List<Emap> {
        val listType = object : TypeToken<List<Emap>>(){}.type
        return GsonBuilder().create().fromJson(emapsString, listType)
    }

    @TypeConverter
    fun fromStringToEmap(value: String): Emap {
        val listType: Type = object : TypeToken<Emap>() {}.type
        return Gson().fromJson(value, listType)
    }

    @TypeConverter
    fun fromEmapToString(list: Emap): String {
        return Gson().toJson(list)
    }

DATABASE

@Database(
    entities = [
        User::class,
        Term::class,
        Emap::class,
        Page::class,
        PageIndex::class,
        Incidencia::class,
        Emap.Poi::class,
        UserEmaps::class,
        CuadernoDeViaje::class
    ], version = 2, exportSchema = false
)
@TypeConverters(Converters::class)
abstract class CanaryDatabase : RoomDatabase()

I have tried to put the TypeConverter in several different places and nothing, nothing has worked for me, if someone knows what is failing it would be of great help.


Solution

  • This answer shows a working solution that you may wish to consider.

    Instead of directly storing a List as a column the column is given a type that has a single field that is a List

    i.e.

    /* Added to simple solution */
    data class EmapListClass(
        val emapList: List<Emap>
    )
    

    This caters for simpler GSON/JSON handling the TypeConverters instead be:-

    @TypeConverter
    fun fromEMapListClassToJSONSTring(emapListClass: EmapListClass):String = Gson().toJson(emapListClass)
    @TypeConverter
    fun fromStringToEmapListClass(jsonString: String): EmapListClass = Gson().fromJson(jsonString,EmapListClass::class.java)
    

    Demo

    To demonstrate consider the following Entities that have been modified to make the demo far simpler (no reliance upon guessing the Types and Type Converters of all the other types not provided):-

    @Entity(tableName = "emaps_users")
    data class UserEmaps(
        @PrimaryKey(autoGenerate = false)
        val userId : Long?=null,
    
        /*@field:TypeConverters(ListEmapConverter::class)*/
        //val emaps : List<Emap> **** ALTERED for simple solution ****
        val emaps: EmapListClass
    )
    
    @Entity(tableName = "eMaps")
    data class Emap(
        @PrimaryKey(autoGenerate = false)
        /*override*/ val id : Int = -1,
        var nombreEMap : String = "",
        /* commented out for simplicity
        var estado : Estado? = Estado.NONE,
        private var listPois : List<Int> = mutableListOf(),
        var isla: Isla? = Isla.NONE,
        var publico : MutableSet<Publico> = mutableSetOf(),
        var tematica : MutableSet<Tematica> = mutableSetOf(),
        var dificultad : MutableSet<Dificultad> = mutableSetOf(),
        var duracion : MutableSet<Duracion> = mutableSetOf(),
        var descripcion : String = "",
        var isCustom : Boolean = true,
        var isPublic : Boolean = false
    
         */
    )
    

    An @Dao annotated interface to cater for the demo:-

    @Dao
    interface AllDAOs {
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(userEmaps: UserEmaps): Long
        @Insert(onConflict = OnConflictStrategy.IGNORE)
        fun insert(emap: Emap): Long
        @Query("SELECT emaps FROM emaps_users WHERE userId = :userId LIMIT 1")
        /*suspend commented out to run on main thread for brevity */ fun getUserEmapsIds(userId : Int) : List<UserEmaps>
    }
    
    • Note that List (as that is the type that is stored in the emaps_users table Emaps are stored in the eMaps table)

    The following @Database annotated abstract class, noting that for brevity of the demo, the main thread is used:-

    @TypeConverters(ListEmapConverter::class)
    @Database(entities = [Emap::class,UserEmaps::class], version = 1, exportSchema = false)
    abstract class CanaryDatabase : RoomDatabase() {
        abstract fun getAllDAOs(): AllDAOs
        companion object {
            private var instance: CanaryDatabase?=null
            fun getInstance(context: Context): CanaryDatabase {
                if (instance==null) {
                    instance=Room.databaseBuilder(context,CanaryDatabase::class.java,"canary.db")
                        .allowMainThreadQueries()
                        .build()
                }
                return instance as CanaryDatabase
            }
        }
    }
    

    Finally some application code that will insert some data and then extract it as per:-

    class MainActivity : AppCompatActivity() {
        lateinit var db: CanaryDatabase
        lateinit var dao: AllDAOs
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            db = CanaryDatabase.getInstance(this)
            dao = db.getAllDAOs()
    
            val emaplist1 = ArrayList<Emap>()
            emaplist1.add(Emap(1,"EM001"))
            emaplist1.add(Emap(2,"EM002"))
            emaplist1.add(Emap(3,"EM003"))
            val elc: EmapListClass = EmapListClass(emaplist1)
            for (el in emaplist1) {
                dao.insert(el)
            }
    
            val emaplist2 = ArrayList<Emap>()
            emaplist2.add(Emap(21,"EM021"))
            emaplist2.add(Emap(22,"EM022"))
            emaplist2.add(Emap(23,"EM023"))
            for (el in emaplist2) {
                dao.insert(el)
            }
    
            val um1 = dao.insert(UserEmaps(emaps = EmapListClass(emaplist1)))
            val um2 = dao.insert(UserEmaps(emaps = EmapListClass(emaplist2)))
    
            /* Retrieving the data from the database */
            val sb = StringBuilder()
            for (um in dao.getUserEmapsIds(um1.toInt())) {
                sb.clear()
                for (em in um.emaps.emapList) {
                    sb.append("\n\tEMAP ID is ${em.id} The NOMBREMAP is ${em.nombreEMap}")
                }
                Log.d("DBINFO","ID is ${um.userId} The EMAPLIST has ${um.emaps.emapList.size} emaps. They are:- ${sb}")
            }
            for (um in dao.getUserEmapsIds(um2.toInt())) {
                sb.clear()
                for (em in um.emaps.emapList) {
                    sb.append("\n\tEMAP ID is ${em.id} The NOMBREMAP is ${em.nombreEMap}")
                }
                Log.d("DBINFO","ID is ${um.userId} The EMAPLIST has ${um.emaps.emapList.size} emaps. They are:- ${sb}")
            }
        }
    }
    

    When run for the first time (the demo is only intended to run once). The output to the log includes (the relevant data):-

    D/DBINFO: ID is null The EMAPLIST has 3 emaps. They are:- 
            EMAP ID is 1 The NOMBREMAP is EM001
            EMAP ID is 2 The NOMBREMAP is EM002
            EMAP ID is 3 The NOMBREMAP is EM003
    D/DBINFO: ID is null The EMAPLIST has 3 emaps. They are:- 
            EMAP ID is 21 The NOMBREMAP is EM021
            EMAP ID is 22 The NOMBREMAP is EM022
            EMAP ID is 23 The NOMBREMAP is EM023
    

    This is expected.

    Using App Inspection then:-

    eMaps table

    and :-

    emaps_user

    • i.e. the list of emaps has been stored as json (the log output showing that the emaps have been rebuilt from the json)

    Note the GSON library used was com.google.code.gson:gson:2.11.0 (there are other GSOn libraries and they may have different features/capabilities). Some may be better suited to handling Lists