142

I just implemented Room for offline data saving. But in an Entity class, I am getting the following error:

Error:(27, 30) error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

And the class is as following:

@Entity(tableName = "firstPageData")
public class MainActivityData {

    @PrimaryKey
    private String userId;

    @ColumnInfo(name = "item1_id")
    private String itemOneId;

    @ColumnInfo(name = "item2_id")
    private String itemTwoId;

    // THIS IS CAUSING THE ERROR... BASICALLY IT ISN'T READING ARRAYS
    @ColumnInfo(name = "mylist_array")
    private ArrayList<MyListItems> myListItems;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public ArrayList<MyListItems> getMyListItems() {
        return myListItems;
    }

    public void setCheckListItems(ArrayList<MyListItems> myListItems) {
        this.myListItems = myListItems;
    }

}

So basically I want to save the ArrayList in the database but I was not able to find anything relevant to it. Can you guide me regarding how to save an Array using Room?

NOTE: MyListItems Pojo class contains 2 Strings (as of now)

starball
  • 20,030
  • 7
  • 43
  • 238
Tushar Gogna
  • 4,963
  • 6
  • 31
  • 42

21 Answers21

166

Type Converter are made specifically for that. In your case, you can use code snippet given below to store data in DB.

public class Converters {
    @TypeConverter
    public static ArrayList<String> fromString(String value) {
        Type listType = new TypeToken<ArrayList<String>>() {}.getType();
        return new Gson().fromJson(value, listType);
    }

    @TypeConverter
    public static String fromArrayList(ArrayList<String> list) {
        Gson gson = new Gson();
        String json = gson.toJson(list);
        return json;
    }
}

And mention this class in your Room DB like this

@Database (entities = {MainActivityData.class},version = 1)
@TypeConverters({Converters.class})

More info here

Matt Ke
  • 3,599
  • 12
  • 30
  • 49
Amit Bhandari
  • 3,014
  • 6
  • 15
  • 33
102

Option #1: Have MyListItems be an @Entity, as MainActivityData is. MyListItems would set up a @ForeignKey back to MainActivityData. In this case, though, MainActivityData cannot have private ArrayList<MyListItems> myListItems, as in Room, entities do not refer to other entities. A view model or similar POJO construct could have a MainActivityData and its associated ArrayList<MyListItems>, though.

Option #2: Set up a pair of @TypeConverter methods to convert ArrayList<MyListItems> to and from some basic type (e.g., a String, such as by using JSON as a storage format). Now, MainActivityData can have its ArrayList<MyListItems> directly. However, there will be no separate table for MyListItems, and so you cannot query on MyListItems very well.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • @TusharGogna: Relationships are covered in [the Room documentation](https://developer.android.com/topic/libraries/architecture/room.html#entities-relationships), and the "entities do not refer directly to other entities" bit is also covered in [the Room documentation](https://developer.android.com/topic/libraries/architecture/room.html#no-object-references). – CommonsWare Jul 08 '17 at 14:12
  • @CommonsWare is `Relation` an option #3 for this case? [Relation documentation](https://developer.android.com/reference/android/arch/persistence/room/Relation.html) – FeleMed Sep 29 '17 at 18:00
  • @FeleMed: Not really. `@Relation` is only for getting things out of the database. It has nothing to do with putting things into the database. – CommonsWare Sep 29 '17 at 18:32
  • 1
    Just as a note. If you're going to persist a list of Int for example, then you need to serialize it as string for option 2. This makes queries more complex. I would rather go for option 1 since is less "type" dependant. – reixa Nov 06 '18 at 10:14
  • You can read alternatives: https://android.jlelse.eu/android-architecture-components-room-relationships-bf473510c14a – Jose Pose S Mar 02 '19 at 13:31
  • 7
    Sometime in the future you may need to query your Items so I'll usually go with option#1 – Jeffrey Mar 10 '19 at 14:20
78

Kotlin version for type converter:

 class Converters {

    @TypeConverter
    fun listToJson(value: List<JobWorkHistory>?) = Gson().toJson(value)

    @TypeConverter
    fun jsonToList(value: String) = Gson().fromJson(value, Array<JobWorkHistory>::class.java).toList()
}

I Used JobWorkHistory object for my purpose, use the object of your own

@Database(entities = arrayOf(JobDetailFile::class, JobResponse::class), version = 1)
@TypeConverters(Converters::class)
abstract class MyRoomDataBase : RoomDatabase() {
     abstract fun attachmentsDao(): AttachmentsDao
}
AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
Manohar
  • 22,116
  • 9
  • 108
  • 144
  • 3
    I think rather than deserializing to an array and then convert to List, it's better to use a List type like this: val listType = object : TypeToken>() {}.type like Amit mentioned in the answer below. – Sohayb Hassoun Sep 01 '18 at 08:57
  • 3
    Also, You might want to fetch cached `Gson` instance from somewhere in your app. Initialising new `Gson` instance on every call can be expensive. – Apsaliya Aug 21 '19 at 09:18
37

Native Kotlin version using Kotlin's serialization component – kotlinx.serialization.

  1. Add the Kotlin serialization Gradle plugin and dependency to your build.gradle:
apply plugin: 'kotlinx-serialization'

dependencies {
   ...
   implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
}
  1. Add the Type converters to your Converter class;
class Converters {
   @TypeConverter
   fun fromList(value : List<String>) = Json.encodeToString(value)

   @TypeConverter
   fun toList(value: String) = Json.decodeFromString<List<String>>(value)
}
  1. Add your Converter class to your database class:
@TypeConverters(Converters::class)
abstract class YourDatabase: RoomDatabase() {...}

And you're done!

Extra resources:

Gilbert
  • 2,699
  • 28
  • 29
Patrick
  • 855
  • 8
  • 12
  • 2
    if auto import not work add: import kotlinx.serialization.json.Json import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString – Fortran Apr 19 '21 at 14:29
  • 6
    You'll probably need `classpath("org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion")` as well – Lennoard Silva May 05 '21 at 20:58
26

Better version of List<String> converter

class StringListConverter {
    @TypeConverter
    fun fromString(stringListString: String): List<String> {
        return stringListString.split(",").map { it }
    }

    @TypeConverter
    fun toString(stringList: List<String>): String {
        return stringList.joinToString(separator = ",")
    }
}
Nitin Misra
  • 4,472
  • 3
  • 34
  • 52
  • 26
    Be ware of using "," as separator as sometimes your string may have the same character and it can be a mess. – emarshah Dec 07 '19 at 16:21
21

Kotlin Answer

You need to do 3 things:

  1. Create Converters class.
  2. Add Converters class on Database.
  3. Just define what you want to use in Entity class.

Usage example step by step:

Step 1 :

 class Converters {

    @TypeConverter
    fun listToJsonString(value: List<YourModel>?): String = Gson().toJson(value)

    @TypeConverter
    fun jsonStringToList(value: String) = Gson().fromJson(value, Array<YourModel>::class.java).toList()
}

Step 2 :

@Database(entities = [YourEntity::class], version = 1)
@TypeConverters(Converters::class)
abstract class YourDatabase : RoomDatabase() {
     abstract fun yourDao(): YourDao
}

Step 3 :

Note: You do not need to call functions of Converter which are listToJsonString() and jsonStringToList(). They are using in background by Room.

@Entity(tableName = "example_database_table") 
data class YourEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  @ColumnInfo(name = "your_model_list") var yourModelList: List<YourModel>,
)
canerkaseler
  • 6,204
  • 45
  • 38
15

I would personally advise against @TypeConverters/serializations, since they break the database's normal forms compliance.

For this particular case it might be worth defining a relationship using the @Relation annotation, which allows to query nested entities into a single object without the added complexity of declaring a @ForeignKey and writing all the SQL queries manually:

@Entity
public class MainActivityData {
    @PrimaryKey
    private String userId;
    private String itemOneId;
    private String itemTwoId;
}

@Entity
public class MyListItem {
    @PrimaryKey
    public int id;
    public String ownerUserId;
    public String text;
}

/* This is the class we use to define our relationship,
   which will also be used to return our query results.
   Note that it is not defined as an @Entity */
public class DataWithItems {
    @Embedded public MainActivityData data;
    @Relation(
        parentColumn = "userId"
        entityColumn = "ownerUserId"
    )
    public List<MyListItem> myListItems;
}

/* This is the DAO interface where we define the queries.
   Even though it looks like a single SELECT, Room performs
   two, therefore the @Transaction annotation is required */
@Dao
public interface ListItemsDao {
    @Transaction
    @Query("SELECT * FROM MainActivityData")
    public List<DataWithItems> getAllData();
}

Aside from this 1-N example, it is possible to define 1-1 and N-M relationships as well.

Albert Gomà
  • 261
  • 2
  • 4
  • 2
    The only sane answer here! Don't violate First Normal Form! – Luke Needham Jan 11 '21 at 17:41
  • I like this solution of using a 1 to N relationship. One question though, if you have a JSON file with some data and you want to store it in your DB, unlike the ArrayList approach which using Gson you can easily create instances of your object holding your data, how do you do it with this data structure? – Emmanuel Murairi May 02 '21 at 10:45
  • 1
    @EmmanuelMurairi I'm afraid you can't. In order to instantiate arbitrary objects at runtime Gson uses [reflection](https://stackoverflow.com/a/10470279/12492043) -which you can use as well- but, as Room works on top of a relational database (SQLite), it structures the data into tables with predefined columns, so you need to know how the data is structured and declare the Entity classes in advance. When you use Gson you are just dumping a huge String into a single column and parsing it at runtime every time you read it. It's a nice workaround, but I try to avoid it as much as I can. – Albert Gomà May 04 '21 at 10:44
  • Sometimes you should, sometimes you shouldn't, depends on whether you need to manipulate it + use it in queries or not – EpicPandaForce Oct 28 '21 at 21:55
  • 1
    @EpicPandaForce Of course, de-normalization some times can lead to a much better performance and many distributed systems take advantage of it. However, one should keep in mind that App requirements may change with newer versions (re-normalizing a de-normalized schema can be a real pain) and that type conversion is a manipulation on its own, which can drain resources (and battery) when not strictly necessary. Only de-normalize when you know what you're doing. – Albert Gomà Oct 30 '21 at 09:52
11

This is how i handle List conversion

public class GenreConverter {
@TypeConverter
public List<Integer> gettingListFromString(String genreIds) {
    List<Integer> list = new ArrayList<>();

    String[] array = genreIds.split(",");

    for (String s : array) {
       if (!s.isEmpty()) {
           list.add(Integer.parseInt(s));
       }
    }
    return list;
}

@TypeConverter
public String writingStringFromList(List<Integer> list) {
    String genreIds = "";
    for (int i : list) {
        genreIds += "," + i;
    }
    return genreIds;
}}

And then on the database i do as shown below

@Database(entities = {MovieEntry.class}, version = 1)
@TypeConverters(GenreConverter.class)

And below is a kotlin implementation of the same;

class GenreConverter {
@TypeConverter
fun gettingListFromString(genreIds: String): List<Int> {
    val list = mutableListOf<Int>()

    val array = genreIds.split(",".toRegex()).dropLastWhile {
        it.isEmpty()
    }.toTypedArray()

    for (s in array) {
        if (s.isNotEmpty()) {
            list.add(s.toInt())
        }
    }
    return list
}

@TypeConverter
fun writingStringFromList(list: List<Int>): String {
    var genreIds=""
    for (i in list) genreIds += ",$i"
    return genreIds
}}
Derrick Njeru
  • 151
  • 1
  • 9
  • I use this solution for simple types (e.g. List, List) because it is lighter than the gson-based solutions. – Julien Kronegg Sep 21 '18 at 08:01
  • 3
    This solution miss the unhappy flow (e.g. null and empty String, null List). – Julien Kronegg Sep 21 '18 at 08:01
  • Yeah I made the mistake of copy pasting this and lost at least an hour to single element lists creating elements with single commas. I have submitted and answer with a fix for that (in Kotlin) – Daniel Wilson May 01 '19 at 15:27
6

Had the same error message as described above. I would like to add: if you get this error message in a @Query, you should add @TypeConverters above the @Query annotation.

Example:

@TypeConverters(DateConverter.class)
@Query("update myTable set myDate=:myDate  where id = :myId")
void updateStats(int myId, Date myDate);

....

public class DateConverter {

    @TypeConverter
    public static Date toDate(Long timestamp) {
        return timestamp == null ? null : new Date(timestamp);
    }

    @TypeConverter
    public static Long toTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}
live-love
  • 48,840
  • 22
  • 240
  • 204
4

This answer uses Kotin to split by comma and construct the comma delineated string. The comma needs to go at the end of all but the last element, so this will handle single element lists as well.

object StringListConverter {
        @TypeConverter
        @JvmStatic
        fun toList(strings: String): List<String> {
            val list = mutableListOf<String>()
            val array = strings.split(",")
            for (s in array) {
                list.add(s)
            }
            return list
        }

        @TypeConverter
        @JvmStatic
        fun toString(strings: List<String>): String {
            var result = ""
            strings.forEachIndexed { index, element ->
                result += element
                if(index != (strings.size-1)){
                    result += ","
                }
            }
            return result
        }
    }
Daniel Wilson
  • 18,838
  • 12
  • 85
  • 135
3

in my case problem was generic type base on this answer

https://stackoverflow.com/a/48480257/3675925 use List instead of ArrayList

 import androidx.room.TypeConverter
 import com.google.gson.Gson 
 import com.google.gson.reflect.TypeToken
 class IntArrayListConverter {
     @TypeConverter
     fun fromString(value: String): List<Int> {
         val type = object: TypeToken<List<Int>>() {}.type
         return Gson().fromJson(value, type)
     }

     @TypeConverter
     fun fromArrayList(list: List<Int>): String {
         val type = object: TypeToken<List<Int>>() {}.type
         return Gson().toJson(list, type)
     } 
}

it doesn't need add @TypeConverters(IntArrayListConverter::class) to query in dao class nor fields in Entity class and just add @TypeConverters(IntArrayListConverter::class) to database class

@Database(entities = [MyEntity::class], version = 1, exportSchema = false)
@TypeConverters(IntArrayListConverter::class)
abstract class MyDatabase : RoomDatabase() {
Hamid Zandi
  • 2,714
  • 24
  • 32
  • This cant work because return type of {Gson().fromJson(arg1, arg2) } needs to return an arraylist of Strings not of Integers,.. {fromJson(arg1, arg2)} requires strings or typetokens – Benja Sep 29 '22 at 11:32
2

If you only want to save and retrieve String list, then I think there is no need for JSON serialization.

Using built-in functions are much simpler and you can avoid reflection too.

Kotlin code:

class Converter {

    @TypeConverter
    fun List<String>.toStringData() = this.joinToString(STRING_SEPARATOR)

    @TypeConverter
    fun String.toList() = this.split(STRING_SEPARATOR)
}

Java code:

public class Converter {

    @TypeConverter
    public static String[] toArray(String data) {
        return data.split(STRING_SEPARATOR);
    }

    @TypeConverter
    public static String toString(String[] data) {
        return String.join(STRING_SEPARATOR, data);
    }
}
Anna M
  • 23
  • 2
1

When we are using TypeConverters then datatype should be return type of TypeConverter method.

For example: TypeConverter method returns a string, then adding table column should be of type string.

private static final Migration MIGRATION_1_2 = new Migration(1, 2) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        // Since we didn't alter the table, there's nothing else to do here.
        database.execSQL("ALTER TABLE "+  Collection.TABLE_STATUS  + " ADD COLUMN deviceType TEXT;");
        database.execSQL("ALTER TABLE "+  Collection.TABLE_STATUS  + " ADD COLUMN inboxType TEXT;");
    }
};
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
1

All above answers are for list of strings. But below helps you to write converter for list of objects.

Just in place of "YourClassName", add your Object class.

 @TypeConverter
        public String fromValuesToList(ArrayList<**YourClassName**> value) {
            if (value== null) {
                return (null);
            }
            Gson gson = new Gson();
            Type type = new TypeToken<ArrayList<**YourClassName**>>() {}.getType();
            return gson.toJson(value, type);
        }
    
        @TypeConverter
        public ArrayList<**YourClassName**> toOptionValuesList(String value) {
            if (value== null) {
                return (null);
            }
            Gson gson = new Gson();
            Type type = new TypeToken<List<**YourClassName**>>() {
            }.getType();
            return gson.fromJson(value, type);
        }
Tarun Anchala
  • 2,232
  • 16
  • 15
0

Adding @TypeConverters with the converter class as params

to Database & to the Dao class, made my queries work

abitcode
  • 1,420
  • 17
  • 24
0

Json conversions don't scale well in terms of memory allocation.I'd rather go for something similar to responses above with some nullability.

class Converters {
    @TypeConverter
    fun stringAsStringList(strings: String?): List<String> {
        val list = mutableListOf<String>()
        strings
            ?.split(",")
            ?.forEach {
                list.add(it)
            }

        return list
    }

    @TypeConverter
    fun stringListAsString(strings: List<String>?): String {
        var result = ""
        strings?.forEach { element ->
            result += "$element,"
        }
        return result.removeSuffix(",")
    }
}

For simple data types the above can be used, otherwise for complex datatypes Room provides Embedded

Dokuzov
  • 148
  • 2
  • 12
0

Here is the example for adding the customObject types to Room DB table. https://mobikul.com/insert-custom-list-and-get-that-list-in-room-database-using-typeconverter/

Adding a type converter was easy, I just needed a method that could turn the list of objects into a string, and a method that could do the reverse. I used gson for this.

public class Converters {

    @TypeConverter
    public static String MyListItemListToString(List<MyListitem> list) {
        Gson gson = new Gson();
        return gson.toJson(list);
    }

    @TypeConverter
    public static List<Integer> stringToMyListItemList(@Nullable String data) {
        if (data == null) {
            return Collections.emptyList();
        }

        Type listType = new TypeToken<List<MyListItem>>() {}.getType();

        Gson gson = new Gson();
        return gson.fromJson(data, listType);
    }
}

I then added an annotation to the field in the Entity:

@TypeConverters(Converters.class)

public final ArrayList<MyListItem> myListItems;
Kebab Krabby
  • 1,574
  • 13
  • 21
0
 @Query("SELECT * FROM business_table")
 abstract List<DatabaseModels.Business> getBusinessInternal();


 @Transaction @Query("SELECT * FROM business_table")
 public ArrayList<DatabaseModels.Business> getBusiness(){
        return new ArrayList<>(getBusinessInternal());
 }
Awscoolboy
  • 36
  • 3
0

All answers above correct. Yes, if you REALLY need store array of something into one SQLite field TypeConverter is a solution.

And I used the accepted answer in my projects.

But don't do it!!!

If you need store array in Entity in 90% cases you need to create one-to-many or many-to-many relationships.

Otherwise, your next SQL query for select something with key inside this array will be absolutely hell...

Example:

Object foo comes as json: [{id: 1, name: "abs"}, {id:2, name: "cde"}

Object bar: [{id, 1, foos: [1, 2], {...}]

So don't make entity like:

@Entity....
data class bar(
...
val foos: ArrayList<Int>)

Make like next:

@Entity(tablename="bar_foo", primaryKeys=["fooId", "barId"])
data class barFoo(val barId: Int, val fooId: Int)

And sore your foos:[] as records in this table.

w201
  • 2,018
  • 1
  • 11
  • 15
  • dont make assumptions if yopu were storing a list of IDs that was available in the first api call but not in the next, then by all means store those ids somewhere and then use them to query the api store it into a table with a junction table, this uses both solutions, i do agree with you that this could be seen as an easy way out and isnt great for a lot of reasons – martinseal1987 Aug 31 '20 at 16:47
-1

I wanted to store a List containing photos URI in ROOM database . Because of special characters I was getting this error localized in my converter class, TypeConverters.class:

com.google.gson.stream.MalformedJsonException: Unterminated array at line 1 column 8 path $[1] 

There was a problem in my simple converter for arrayList of String, in fact it was :

public static List<String> fromString(String value) {
    Type listType = new TypeToken<ArrayList<String>>() {
    }.getType();
    return new Gson().fromJson(value, listType);
}

@TypeConverter
public static String fromArrayList(List<String> list) {
    Gson gson = new Gson();
    return gson.toJson(list);
}

By taking @Derrick Njeru comment back, I changed it for that it could take in consideration a String as "https://images.app.goo.gl/jwhkhzhZVWrceQV67" like this :

    @TypeConverter
public List<String> gettingListFromString(String genreIds) {
    List<String> list = new ArrayList<>();

    String[] array = genreIds.split(",");
    
    for (String s : array) {
        if (!s.isEmpty()) {
            list.add(s);
        }
    }
    return list;
}

@TypeConverter
public String writingStringFromList(List<String> list) {
    String genreIds = "";
    for (String i : list) {
        genreIds += "," + i;
    }
    return genreIds;
}
-3

Use official solution from room, @Embedded annotation :

@Embedded(prefix = "mylist_array") private ArrayList<MyListItems> myListItems
Fidan Bacaj
  • 408
  • 5
  • 10