I'm getting response from server based on such structure:
{
"success":true,
"data":{"can be some kind of data, array or error message"}
}
What is the right way to map properly data attribute in such situations?
My attempts was to use Any type and cast after to specified type:
data class GeneralResponseModel(
val success: Boolean,
val data: Any
)
Provider
//
val response = gson.fromJson(it[0].toString(), GeneralResponseModel::class.java)
//
ViewModel
////////
if (res.success) {
isLoading.postValue(false)
///////
} else {
val result = res.data as ResponseError
errorMessage.postValue(ErrorWrapper(ErrorType.REQUEST_ERROR,result.detail,result.title))
isLoading.postValue(false)
}
///////////
And I got
io.reactivex.exceptions.OnErrorNotImplementedException:
com.google.gson.internal.LinkedTreeMap cannot be cast to
com.myapp.model.response.ResponseError
Another attempt was in using empty interface which was implemented by all possible response types. In this situation I got
java.lang.RuntimeException: Unable to invoke no-args constructor for
interface com.myapp.model.response.Response.
Registering an InstanceCreator with Gson for this type may fix this
problem.
I'm not sure about proper way to handle such trivial case. Any links, code examples or help appreciated. Thanks in advance.
Update
Thanks to Niklas I reconsidered gson into such structure:
lateinit var gson: Gson
when (methodName) {
RequestList.LOGIN.methodName -> {
gson =
GsonBuilder().registerTypeAdapter(
GeneralResponseModel::class.java,
object : JsonDeserializer<GeneralResponseModel> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): GeneralResponseModel {
val gsonInner = Gson()
val jsonObject: JsonObject = json!!.asJsonObject
lateinit var generalResponseModel: GeneralResponseModel
generalResponseModel = if (!jsonObject.get("success").asBoolean) {
GeneralResponseModel(
false,
gsonInner.fromJson(jsonObject.get("data"), ResponseError::class.java)
)
} else {
GeneralResponseModel(
true,
gsonInner.fromJson(jsonObject.get("data"), DriverData::class.java)
)
}
return generalResponseModel
}
}).create()
}
RequestList.GET_JOBS.methodName -> {
gson = GsonBuilder().registerTypeAdapter(
GeneralResponseModel::class.java,
object : JsonDeserializer<GeneralResponseModel> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): GeneralResponseModel {
val gsonInner = Gson()
val jsonObject: JsonObject = json!!.asJsonObject
lateinit var generalResponseModel: GeneralResponseModel
generalResponseModel = if (!jsonObject.get("success").asBoolean) {
GeneralResponseModel(
false,
gsonInner.fromJson(jsonObject.get("data"), ResponseError::class.java)
)
} else {
GeneralResponseModel(
true,
gsonInner.fromJson(jsonObject.get("data"), Array<JobResponse>::class.java)
)
}
return generalResponseModel
}
}).create()
}
else -> gson = Gson()
}
Since your data is very generic, it can't really be parsed in a type-safe way. Gson can't infer types based on pure text (in your specific case there is nothing telling Gson that your data is a ResponseError).
I would consider a general wrapper class like yours, and then using a GSON TypeAdapter to parse the response to your general wrapper.
You have to instantiate GSON with a Builder to define a custom TypeAdapter.
registerTypeAdapter(Type type, Object typeAdapter)
Configures Gson for custom serialization or deserialization.
Your wrapper:
public class Response<T> {
T data;
String message;
public Response(T data, String message) {
this.data = data;
this.message = message;
}
boolean hasData() {
return data != null;
}
T getData() {
return data;
}
String getMessage() {
return message;
}
}
Initialization:
GsonBuilder builder = new GsonBuilder();
b.registerTypeAdapter(Response.class, new JsonDeserializer<Response>() {
#Override
public Response deserialize(JsonElement arg0, Type arg1,
JsonDeserializationContext arg2) throws JsonParseException {
// ... create Response object here
return response;
}
Related
I have looked into this issue, and tried to implement what i could find over the SO, but no luck. So is my problem:
Sometimes, in my JSON response, I get the data field as a JSON object, and other times the data field as JSON array. Technically this is a bad API design, however changing the API at this point is not feasible.
Two types of JSON responses by API
{
data: {
.
.
.
}
}
{
data : [
.
.
.
]
}
I have tried to implement the DataDeserializer, however the GSON still identifies the JSON response as JSON object and not able to use the DataDeserializer:
Class DataDeserializer
class DataDeserializer : JsonDeserializer<List<Data<Any>>> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
context: JsonDeserializationContext
): List<Data<Any>> {
val dataList = ArrayList<Data<Any>>()
when {
json.isJsonObject -> {
val data = context.deserialize<Data<Any>>(json.asJsonObject, Data::class.java)
dataList.add(data)
}
json.isJsonArray -> {
for (jsonObject in json.asJsonArray)
dataList.add(context.deserialize(jsonObject, Data::class.java))
}
else -> throw RuntimeException("Unexpected JSON Type: ${json.javaClass}")
}
return dataList
}
}
Model Classes
open class Json<T> {
lateinit var data: List<Data<T>>
fun getFirstChild() = data.first()
}
data class Data<T>(
private val id: String = "",
private val type: String = "",
val attributes: T
)
Registering DataDeserializer with GSONConverterFactory
val gson = GsonBuilder().registerTypeAdapter(Data::class.java, DataDeserializer()).create()
My API sends me a polyphonic Json in with the variable addon_item can be either a String or an Array, I have spend days trying to make a CustomDezerializer for it without any success.
Here is the Json response:
({
"code": 1,
"msg": "OK",
"details": {
"merchant_id": "62",
"item_id": "1665",
"item_name": "Burrito",
"item_description": "Delicioso Burrito en base de tortilla de 30 cm",
"discount": "",
"photo": "http:\/\/www.asiderapido.cloud\/upload\/1568249379-KDKQ5789.jpg",
"item_cant": "-1",
"cooking_ref": false,
"cooking_ref_trans": "",
"addon_item": [{
"subcat_id": "144",
"subcat_name": "EXTRA",
"subcat_name_trans": "",
"multi_option": "multiple",
"multi_option_val": "",
"two_flavor_position": "",
"require_addons": "",
"sub_item": [{
"sub_item_id": "697",
"sub_item_name": "Queso cheddar",
"item_description": "Delicioso queso fundido",
"price": "36331.20",
"price_usd": null
}]
}]
}
})
Here is the Custom Dezerializer, which includes BodyConverter that removes two braces that encompassed the Json response:
'''
/**
* This class was created due to 2 issues with the current API responses:
* 1. The API JSON results where encapsulated by parenthesis
* 2. They had dynamic JSON variables, where the Details variable was coming as a String
* or as an Object depending on the error message (werer whe user and password wereh correct.
*
*/
class JsonConverter(private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(
type: Type?, annotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<ResponseBody, *>? {
val adapter = gson.getAdapter(TypeToken.get(type!!))
return GsonResponseBodyConverter(gson, adapter)
}
override fun requestBodyConverter(
type: Type?,
parameterAnnotations: Array<Annotation>?,
methodAnnotations: Array<Annotation>?,
retrofit: Retrofit?
): Converter<*, RequestBody>? {
val adapter = gson.getAdapter(TypeToken.get(type!!))
return GsonRequestBodyConverter(gson, adapter)
}
internal inner class GsonRequestBodyConverter<T>(
private val gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<T, RequestBody> {
private val MEDIA_TYPE = MediaType.parse("application/json; charset=UTF-8")
private val UTF_8 = Charset.forName("UTF-8")
#Throws(IOException::class)
override fun convert(value: T): RequestBody {
val buffer = Buffer()
val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return RequestBody.create(MEDIA_TYPE, buffer.readByteString())
}
}
// Here we remove the parenthesis from the JSON response
internal inner class GsonResponseBodyConverter<T>(
gson: Gson,
private val adapter: TypeAdapter<T>
) : Converter<ResponseBody, T> {
#Throws(IOException::class)
override fun convert(value: ResponseBody): T? {
val dirty = value.string()
val clean = dirty.replace("(", "")
.replace(")", "")
try {
return adapter.fromJson(clean)
} finally {
value.close()
}
}
}
class DetalleDeProductoDeserializer : JsonDeserializer<DetallesDelItemWrapper2> {
override fun deserialize(
json: JsonElement,
typeOfT: Type,
context: JsonDeserializationContext
): DetallesDelItemWrapper2 {
if ((json as JsonObject).get("addon_item") is JsonObject) {
return Gson().fromJson<DetallesDelItemWrapper2>(json, ListaDetalleAddonItem::class.java)
} else {
return Gson().fromJson<DetallesDelItemWrapper2>(json, DetallesDelItemWrapper2.CookingRefItemBoolean::class.java)
}
}
}
companion object {
private val LOG_TAG = JsonConverter::class.java!!.getSimpleName()
fun create(detalleDeProductoDeserializer: DetalleDeProductoDeserializer): JsonConverter {
Log.e("Perfill Adapter = ", "Test5 " + "JsonConverter" )
return create(Gson())
}
fun create(): JsonConverter {
return create(Gson())
}
private fun create(gson: Gson?): JsonConverter {
if (gson == null) throw NullPointerException("gson == null")
return JsonConverter(gson)
}
}
}
Here is the RetrofitClient.class:
class RetrofitClient private constructor(name: String) {
private var retrofit: Retrofit? = null
fun getApi(): Api {
return retrofit!!.create(Api::class.java)
}
init {
if (name == "detalleDelItem") run {
retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(JsonConverterJava.create(JsonConverterJava.DetallesDelItemDeserializer()))
// .addConverterFactory(GsonConverterFactory.create(percentDeserializer))
.client(unsafeOkHttpClient.build())
.build()
Log.e("RetrofitClient ", "Instace: " + "detalle " + name)
}
}
companion object {
//Remember this shit is https for the production server
private val BASE_URL = "http://www.asiderapido.cloud/mobileapp/api/"
private var mInstance: RetrofitClient? = null
#Synchronized
fun getInstance(name: String): RetrofitClient {
mInstance = RetrofitClient(name)
return mInstance!!
}
}
}
Finally my POJO:
open class DetallesDelItemWrapper2 {
#SerializedName("code")
val code: Int? = null
#Expose
#SerializedName("details")
var details: ItemDetails? = null
#SerializedName("msg")
val msg: String? = null
class ItemDetails {
#Expose
#SerializedName("addon_item")
val addonItem: Any? = null
#SerializedName("category_info")
val categoryInfo: CategoryInfo? = null
#SerializedName("cooking_ref")
val cookingRef: Any? = null
#SerializedName("cooking_ref_trans")
val cookingRefTrans: String? = null
}
class ListaDetalleAddonItem: DetallesDelItemWrapper2(){
#SerializedName("addon_item")
val detalleAddonItem: List<DetalleAddonItem>? = null
}
class StringDetalleAddonItem: DetallesDelItemWrapper2(){
#SerializedName("addon_item")
val detalleAddonItem: String? = null
}
I took a shot at this and came up with 2 possible ideas. I don't think they're the only way to achieve this, but I think I can share my thoughts.
First, I've reduced the problem to actually only parsing the items. So I've removed retrofit from the equation and use the following jsons:
val json = """{
"addon_item": [{
"subcat_id": "144",
"subcat_name": "EXTRA",
"subcat_name_trans": "",
"multi_option": "multiple",
"multi_option_val": "",
"two_flavor_position": "",
"require_addons": "",
"sub_item": [{
"sub_item_id": "697",
"sub_item_name": "Queso cheddar",
"item_description": "Delicioso queso fundido",
"price": "36331.20",
"price_usd": null
}]
}]
}
""".trimIndent()
(for when the addon_item is an array)
val jsonString = """{
"addon_item": "foo"
}
""".trimIndent()
(for when the addon_item is a string)
First approach
My first approach was to model addon_item as a generic JsonElement:
data class ItemDetails(
#Expose
#SerializedName("addon_item")
val addonItem: JsonElement? = null
)
(I'm using data classes because I find them more helpful, but you don't have too)
The idea here is to let gson deserialize it as a generic json element and you can then inspect it yourself. So if we add some convenience methods to the class:
data class ItemDetails(
#Expose
#SerializedName("addon_item")
val addonItem: JsonElement? = null
) {
fun isAddOnItemString() =
addonItem?.isJsonPrimitive == true && addonItem.asJsonPrimitive.isString
fun isAddOnItemArray() =
addonItem?.isJsonArray == true
fun addOnItemAsString() =
addonItem?.asString
fun addOnItemAsArray() =
addonItem?.asJsonArray
}
So as you can see, we check the addOnItem for what it contains and according to that, we can obtain its contents. Here's an example of how to use it:
fun main() {
val item = Gson().fromJson(jsonString, ItemDetails::class.java)
println(item.isAddOnItemArray())
println(item.isAddOnItemString())
println(item.addOnItemAsString())
}
I think the biggest advantage of this is that it's fairly simple and you don't require custom logic to deserialize. For me, the huge drawback is the type-safety loss.
You can get the add on as an array, but it will be an array of json elements that have to be "manually" deserialized. Hence, my 2nd approach tries to tackle this.
Second approach
The idea here is to use Kotlin's sealed classes and have 2 types of add ons:
sealed class AddOnItems {
data class StringAddOnItems(
val addOn: String
) : AddOnItems()
data class ArrayAddOnItems(
val addOns: List<SubCategory> = emptyList()
) : AddOnItems()
fun isArray() = this is ArrayAddOnItems
fun isString() = this is StringAddOnItems
}
The SubCategory class is just what was inside the list. Here's a simple version of it:
data class SubCategory(
#SerializedName("subcat_id")
val id: String
)
As you can see the AddOnItems is a sealed class that has the only 2 possible types for your use case.
Now we need a custom deserializer:
class AddOnItemsDeserializer : JsonDeserializer<AddOnItems> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?) =
when {
json?.isJsonArray == true -> {
AddOnItems.ArrayAddOnItems(context!!.deserialize(
json.asJsonArray,
TypeToken.getParameterized(List::class.java, SubCategory::class.java).type))
}
json?.isJsonPrimitive == true && json.asJsonPrimitive.isString ->
AddOnItems.StringAddOnItems(json.asJsonPrimitive.asString)
else -> throw IllegalStateException("Cannot parse $json as addonItems")
}
}
In a nutshell, this checks if add on is an array and creates the respective class and the same for string.
Here's how you can use it:
fun main() {
val item = GsonBuilder()
.registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
.create()
.fromJson(jsonString, ItemDetails::class.java)
println(item.addOnItems.isString())
println(item.addOnItemsAsString().addOn)
val item = GsonBuilder()
.registerTypeAdapter(AddOnItems::class.java, AddOnItemsDeserializer())
.create()
.fromJson(json, ItemDetails::class.java)
println(item.addOnItems.isArray())
println(item.addOnItemsAsArray().addOns[0])
}
I think the biggest advantage here is that you get to keep the types. However, you still need to check what it is before calling addOnItemsAs*.
Hope this helps
[
{
"account":{
"availableBalanceInCents":0.0,
"unitCredits":[
],
"accountId":2001003318,
"currentBalanceInCents":0.0,
"reservations":[],
"accountSummary":{},
"accountHistory":{},
"status":8
}
},
{
"account":{
"availableBalanceInCents":0.0,
"unitCredits":[],
"accountId":2001003318,
"currentBalanceInCents":0.0,
"reservations":[],
"accountSummary":{},
"accountHistory":{},
"status":8
}
},
{},
{}
]
I want to get list of account using gson deserialize. Pls help how can I ignore the empty object.
I've done something like this till now, but it returns the empty objects.
val balance =
GsonBuilder()
.create()
.fromJson<ArrayList<BalanceDetail>>(
JSONArray(data)
.toString(),
object : TypeToken<List<BalanceDetail>>() {}.type
)
You can use wrapper:
data class BalanceDetailWrapper(
#SerializedName("account") val account: BalanceDetail?
)
And deserialize like this:
val type = object : TypeToken<List<BalanceDetailWrapper>>() {}.type
val balance = GsonBuilder().create()
.fromJson<ArrayList<BalanceDetailWrapper>>(JSONArray(data).toString(), type)
Try:
val balance =
GsonBuilder()
.registerTypeHierarchyAdapter(
BalanceDetail::class.java,
object : JsonDeserializer<BalanceDetail?> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): BalanceDetail? {
return if (json?.asJsonObject?.entrySet()?.size == 0) {
null
} else {
Gson().fromJson(json, BalanceDetail::class.java)
}
}
})
.create()
.fromJson<ArrayList<BalanceDetail>>(
JSONArray(value)
.toString(),
object : TypeToken<List<BalanceDetail>>() {}.type
)
In my opinion you need just to filter out null elements with List.filterNotNull() function
I am making an API request which returns some array values. I need to serialize these array values so that I can assign them to their corresponding class attributes (which are String types).
Now I know how to use GSON to serialize and deserialize lists, but with Retrofit the mapping is done automatically. This means that if my attribute is of type String, the API call returns the error "Expected a String but received an Array instead". How do I get around this so that I can receive them as arrays without failure, and them store them as strings subsequently?
My API Response:
{
"utterances": [{
"langs": ["eng", "afr", "xho", "zul"],
"utts": [
"Have you been here before?",
"Was u al hier gewees?",
"Ingaba wakhe weza apha ngaphambili?",
"Ingabe uke weza lapha ngaphambilini?"
],
"responses": [
["Yes", "No"],
["Ja", "Nee"],
["Ewe", "Hayi"],
["Yebo", "Cha"]
]
},
{
"langs": ["eng", "afr", "xho", "zul"],
"utts": [
"How are you?",
"Hoe gaan dit met jou?",
"unjani?",
"unjani?"
],
"responses": [
["Good", "Bad"],
["Goed", "sleg"],
["ezilungileyo", "ezimbi"],
["kuhle", "kubi"]
]
}
]
}
My UtteranceResponse class:
class UtteranceResponse {
#SerializedName("status")
var status: String? = null
#SerializedName("count")
var count: Int = 0
#SerializedName("utterances")
var utterances: ArrayList<Utterance>? = null
}
My Utterance class:
class Utterance: SugarRecord {
#SerializedName ("langs")
var langs: String? = null
#SerializedName ("utts")
var utterances_text: String? = null
var utterances_tts: String? = null
#SerializedName ("responses")
var responses_text: String? = null
constructor(){
}
}
And finally the calling function:
fun getUtterancesFromWebservice (){
val apiService = ApiInterface.create()
val call = apiService.getUtteranceDetails()
call.enqueue(object: Callback<UtteranceResponse> {
override fun onResponse(call: Call<UtteranceResponse>, response: retrofit2.Response<UtteranceResponse>?) {
if (response != null) {
if (response.body()?.utterances != null){
var list: List<Utterance> = response.body()?.utterances!!
val utterances: Utterance = list[0]
//storeUtterancesFromList(list)
} else {
Log.d ("Response:", response.body().toString())
}
}else{
Log.d ("responseResult", "NULL")
}
}
override fun onFailure(call: Call<UtteranceResponse>, t: Throwable) {
Log.e("SHIT", t.toString())
}
})
}
UPDATE
My API Interface as well:
#GET("bins/1ahazo")
abstract fun getUtteranceDetails():Call<UtteranceResponse>
companion object Factory {
const val BASE_URL = "https://api.myjson.com/"
fun create(): ApiInterface {
val gson = GsonBuilder().setPrettyPrinting().create()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
return retrofit.create(ApiInterface::class.java)
}
}
You are returning single object not list. Change Call<UtteranceResponse> in ApiInterface to
Call<List<Utterance>>
and for converting list to string list to string and string to list
class Utterance: SugarRecord {
#SerializedName ("langs")
var langs: List<String?>? = null
#SerializedName ("utts")
var utterances_text: String? = null
var utterances_tts: List<String?>? = null
#SerializedName ("responses")
var responses_tex:List<List<String?>?>? = null;
constructor(){
}
}
Im not able to create a typeConverter in room due to an error. I seem to be following everything per the docs. I would like to convert a list to a json string. lets take a look at my entity:
#Entity(tableName = TABLE_NAME)
public class CountryModel {
public static final String TABLE_NAME = "Countries";
#PrimaryKey
private int idCountry;
/* I WANT TO CONVERT THIS LIST TO A JSON STRING */
private List<CountryLang> countryLang = null;
public int getIdCountry() {
return idCountry;
}
public void setIdCountry(int idCountry) {
this.idCountry = idCountry;
}
public String getIsoCode() {
return isoCode;
}
public void setIsoCode(String isoCode) {
this.isoCode = isoCode;
}
public List<CountryLang> getCountryLang() {
return countryLang;
}
public void setCountryLang(List<CountryLang> countryLang) {
this.countryLang = countryLang;
}
}
The country_lang is what i would like to convert to a string json. So i created the following converter:
Converters.java:
public class Converters {
#TypeConverter
public static String countryLangToJson(List<CountryLang> list) {
if(list == null)
return null;
CountryLang lang = list.get(0);
return list.isEmpty() ? null : new Gson().toJson(lang);
}}
then the problem is anywhere i put the #TypeConverters({Converters.class}) i keep getting an error. But officially this is where i have placed the annotation to get the typeConverter registered:
#Database(entities = {CountryModel.class}, version = 1 ,exportSchema = false)
#TypeConverters({Converters.class})
public abstract class MYDatabase extends RoomDatabase {
public abstract CountriesDao countriesDao();
}
The error i get is:
Error:(58, 31) error: Cannot figure out how to save this field into database. You can consider adding a type converter for it.
This is a common problem I've seen since Room was announced. Room does not support the ability to store Lists directly, nor the ability to convert to/from Lists. It supports converting and storing POJO's.
In this case the solution is simple. Instead of storing a List<CountryLang> you want to store CountryLangs (note the 's')
I've done a quick example of a solution here :
public class CountryLangs {
private List<String> countryLangs;
public CountryLangs(List<String> countryLangs) {
this.countryLangs = countryLangs;
}
public List<String> getCountryLangs() {
return countryLangs;
}
public void setCountryLangs(List<String> countryLangs) {
this.countryLangs = countryLangs;
}
}
This POJO is an inversion of your previous object. It is an object that stores a list of languages. Instead of a list of objects that store your language.
public class LanguageConverter {
#TypeConverter
public CountryLangs storedStringToLanguages(String value) {
List<String> langs = Arrays.asList(value.split("\\s*,\\s*"));
return new CountryLangs(langs);
}
#TypeConverter
public String languagesToStoredString(CountryLangs cl) {
String value = "";
for (String lang :cl.getCountryLangs())
value += lang + ",";
return value;
}
}
This converter takes a list of strings and converts them into a comma seperated string to be stored in a single column. When it fetches the string from the SQLite db to convert back, it splits the list on commas, and populates the CountryLangs.
Insure to update your RoomDatabase version after making these changes.You have the rest of the configuration correct. Happy hunting with the rest of your Room persistence work.
Had same error: "Cannot figure out how to save this field into database" while trying to add a Date field. Had to add a converter class for it and add #TypeConverters annotation to field.
Example:
WordEntity.java
import androidx.room.TypeConverters;
#Entity
public class WordEntity {
#PrimaryKey(autoGenerate = true)
public int id;
private String name;
#TypeConverters(DateConverter.class)
private Date createDate;
...
}
DateConverter.java:
import androidx.room.TypeConverter;
import java.util.Date;
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();
}
}
I used the type converters described here (Article at Medium.com) and it worked:
#TypeConverter
public static List<MyObject> storedStringToMyObjects(String data) {
Gson gson = new Gson();
if (data == null) {
return Collections.emptyList();
}
Type listType = new TypeToken<List<MyObject>>() {}.getType();
return gson.fromJson(data, listType);
}
#TypeConverter
public static String myObjectsToStoredString(List<MyObject> myObjects) {
Gson gson = new Gson();
return gson.toJson(myObjects);
}
Just in case if you need more clarity.
First create a converter general class like below.
class Converters {
#TypeConverter
fun fromGroupTaskMemberList(value: List<Comment>): String {
val gson = Gson()
val type = object : TypeToken<List<Comment>>() {}.type
return gson.toJson(value, type)
}
#TypeConverter
fun toGroupTaskMemberList(value: String): List<Comment> {
val gson = Gson()
val type = object : TypeToken<List<Comment>>() {}.type
return gson.fromJson(value, type)
}
}
Then add this converter in data base class just like,
#TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
just annotate that object with #Embedded solved my issue . Like this
#Embedded
private List<CrewListBean> crewList;
I might be late to answer but.I have some simple solution for this i am sharing the TypeConverters call which handle some basice requirement
class RoomConverters {
//for date and time convertions
#TypeConverter
fun calendarToDateStamp(calendar: Calendar): Long = calendar.timeInMillis
#TypeConverter
fun dateStampToCalendar(value: Long): Calendar =
Calendar.getInstance().apply { timeInMillis = value }
//list of cutome object in your database
#TypeConverter
fun saveAddressList(listOfString: List<AddressDTO?>?): String? {
return Gson().toJson(listOfString)
}
#TypeConverter
fun getAddressList(listOfString: String?): List<AddressDTO?>? {
return Gson().fromJson(
listOfString,
object : TypeToken<List<String?>?>() {}.type
)
}
/* for converting List<Double?>? you can do same with other data type*/
#TypeConverter
fun saveDoubleList(listOfString: List<Double>): String? {
return Gson().toJson(listOfString)
}
#TypeConverter
fun getDoubleList(listOfString: List<Double>): List<Double> {
return Gson().fromJson(
listOfString.toString(),
object : TypeToken<List<Double?>?>() {}.type
)
}
// for converting the json object or String into Pojo or DTO class
#TypeConverter
fun toCurrentLocationDTO(value: String?): CurrentLocationDTO {
return Gson().fromJson(
value,
object : TypeToken<CurrentLocationDTO?>() {}.type
)
}
#TypeConverter
fun fromCurrentLocationDTO(categories: CurrentLocationDTO?): String {
return Gson().toJson(categories)
}
}
you have to write own classes and parse in here after that add it to your AppDatabase class
#Database(
entities = [UserDTO::class],
version = 1, exportSchema = false
)
#TypeConverters(RoomConverters::class)
#Singleton
abstract class AppDatabase : RoomDatabase() {
#TypeConverter doesn't recognize List class, so you should use ArrayList instead, so you don't need additional wrapper for the list you want to persist.
You can avoid object to string(json) type converter for all objects using only this type converter
#TypeConverter
fun objectToJson(value: Any?) = Gson().toJson(value)
I use this and has to only define converter for string(json) to object
eg.
#TypeConverter
fun stringToPersonObject(string: String?): Person? {
return Gson().fromJson(string, Person::class.java)
}
Kotlin example (not good but simple, TODO: json):
import android.arch.persistence.room.*
#Entity(tableName = "doctor")
data class DoctorEntity(
#PrimaryKey
#ColumnInfo(name = "id") val id: Long,
#ColumnInfo(name = "contactName") val contactName: String?,
#TypeConverters(CategoryConverter::class)
#ColumnInfo(name = "categories") val categories: Categories?,
#TypeConverters(CategoryConverter::class)
#ColumnInfo(name = "languages") val languages: Categories?
)
data class Categories(
val categories: ArrayList<Long> = ArrayList()
)
class CategoryConverter {
#TypeConverter
fun toCategories(value: String?): Categories {
if (value == null || value.isEmpty()) {
return Categories()
}
val list: List<String> = value.split(",")
val longList = ArrayList<Long>()
for (item in list) {
if (!item.isEmpty()) {
longList.add(item.toLong())
}
}
return Categories(longList)
}
#TypeConverter
fun toString(categories: Categories?): String {
var string = ""
if (categories == null) {
return string
}
categories.categories.forEach {
string += "$it,"
}
return string
}
}
One can organize back & forth #TypeConverter as #TypeConverters:
public class DateConverter {
#TypeConverter
public long from(Date value) {
return value.getTime();
}
#TypeConverter
public Date to(long value) {
return new Date(value);
}
}
And then apply them to fields with:
#TypeConverters(DateConverter.class)
If you want to make a custom class compatible (different from the supported ones) you must provide a bidirectional #TypeConverter converter which converts the custom one to one known by Room and vice versa.
For example, if we want to keep instances of LatLng:
Precondition: implementation("com.squareup.moshi:moshi-kotlin:1.9.2")
Converters.kt
#TypeConverter
fun stringToLatLng(input: String?): LatLng? =
input?.let { Moshi.Builder().build().adapter(LatLng::class.java).fromJson(it) }
#TypeConverter
fun latLngToString(input: LatLng): String? =
Moshi.Builder().build().adapter(LatLng::class.java).toJson(input)
Room already knows how to store a String.
With these converters, you can use your custom types in other
queries, just as you would with primitive types
Location.kt
#Entity
data class Location(private val location: LatLng?)
GL
Source
Solutions given here are incomplete, once you complete the process given here in the accepted answer you need to add another annotation in you Entity class as well
#TypeConverters(Converter.class)
private List<String> brandId;
This should be put on the element which is causing the error in Room DB
happy coding
You also have to create this TypeConverter which will convert your List to String,
#TypeConverter
public List<CountryLang> toCountryLangList(String countryLangString) {
if (countryLangString == null) {
return (null);
}
Gson gson = new Gson();
Type type = new TypeToken<List<CountryLang>>() {}.getType();
List<CountryLang> countryLangList = gson.fromJson(countryLangString, type);
return countryLangList;
}
And for more info you can also check my another answer.
Had the same issue. Change the List to ArrayList. The list is an interface and room is unable to store it.