I am fetching a JSON object which contains a generic member (data can be of a few different types). The class currently looks like this:
#Parcelize
data class Children<T: Parcelable>(
#Json(name = "type") val type: String,
#Json(name = "data") val data: T
): Parcelable
How do I go about being able to deserialize/map the correct object type with moshi?
#Parcelize
data class Comment<T : Parcelable>(
#Json(name = "replies") val replies: Children<T>,
#Json(name = "count") val count: Int,
#Json(name = "children") val childs: List<String>
) : Parcelable
Or how about instances such as this? I should note Comment can take a generic param of Comment thus resulting in a loop.
Add below inlines in an MoshiExtensions and try to use them accordingly.
inline fun <reified E> Moshi.listAdapter(elementType: Type = E::class.java): JsonAdapter<List<E>> {
return adapter(listType<E>(elementType))
}
inline fun <reified K, reified V> Moshi.mapAdapter(
keyType: Type = K::class.java,
valueType: Type = V::class.java): JsonAdapter<Map<K, V>> {
return adapter(mapType<K, V>(keyType, valueType))
}
inline fun <reified E> listType(elementType: Type = E::class.java): Type {
return Types.newParameterizedType(List::class.java, elementType)
}
inline fun <reified K, reified V> mapType(
keyType: Type = K::class.java,
valueType: Type = V::class.java): Type {
return Types.newParameterizedType(Map::class.java, keyType, valueType)
}
Related
I have following class hierarchy Github Sample
interface OptionV2 {
val id: String
}
#JsonClass(generateAdapter = true)
class ImageSelectionOption(
override val id: String,
value: String,
#Json(name = "active_image")
val image: String?,
): OptionV2
#JsonClass(generateAdapter = true)
class QuestionResponse<T> (
override val id: String,
val answer: T?,
): OptionV2
And following test
val childOptions = listOf(ImageSelectionOption(value = "dummy", id = "dummy", image = "dummy"))
val childResponse = QuestionResponse<List<OptionV2>>(answer = childOptions, id = "child_qid")
val parentOptions = listOf(childResponse)
val parentResponse = QuestionResponse<Any>(answer = parentOptions, id = "parent_qid")
val moshi = Moshi.Builder().add(OptionV2MoshiAdapter.OptionAdapterFactory).build()
val type = Types.newParameterizedType(QuestionResponse::class.java, Any::class.java)
moshi.adapter<QuestionResponse<Any>>(type).toJson(parentResponse)
I am essentially attempting to deserialize QuestionResponse<List<QuestionResponse<List<Option>>>> type. This fails with following error
Failed to find the generated JsonAdapter constructor for 'class dev.abhishekbansal.moshilistinheritance.QuestionResponse'. Suspiciously, the type was not parameterized but the target class 'dev.abhishekbansal.moshilistinheritance.QuestionResponseJsonAdapter' is generic. Consider using Types#newParameterizedType() to define these missing type variables.
I wish to be able to write a custom adapter for this if needed. As I need to be able to deserialize this in the Retrofit scenario.
Here is more complete Github Sample
Update
Finally got it working by using this
// List<Option>
val listType = Types.newParameterizedType(List::class.java, OptionV2::class.java)
// QuestionResponse<List<Option>>
val qr1 = Types.newParameterizedType(QuestionResponse::class.java, listType)
// List<QuestionResponse<List<Option>>>
val listType2 = Types.newParameterizedType(List::class.java, qr1)
// QuestionResponse<List<QuestionResponse<List<Option>>>>
val finalType = Types.newParameterizedType(QuestionResponse::class.java, listType2)
println(moshi.adapter<QuestionResponse<Any>>(finalType).toJson(parentResponse))
I am still confused about how can I write a custom adapter for this which can be supplied to Moshi instance which is supplied to Retrofit. So that it can be serialized on the fly.
Here is the Custom Adapter that worked for me. I have a couple of doubts in this but it works.
class QuestionResponseAdapter<T>(val elementAdapter: JsonAdapter<T>) : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T? {
return elementAdapter.fromJson(reader)
}
override fun toJson(writer: JsonWriter, value: T?) {
elementAdapter.toJson(writer, value)
}
object QuestionResponseAdapterFactory : Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
if (!annotations.isEmpty()) {
return null // Annotations? This factory doesn't apply.
}
if (type !== QuestionResponse::class.java) {
return null // Not a QuestionResponse This factory doesn't apply.
}
// Handle Type erasure at runtime, this class does not need adapter with single level of generic though
val parameterizedType = Types.newParameterizedType(type, Any::class.java)
val elementAdapter: JsonAdapter<Any> = moshi.adapter(parameterizedType)
return QuestionResponseAdapter(elementAdapter).nullSafe()
}
}
}
In my case I added a similar custom method.
protected inline fun <reified T> convert(value: SomeGenericClass<T>): String {
val parameterizedType = Types.newParameterizedType(SomeGenericClass::class.java, T::class.java)
val adapter = moshi.adapter<SomeGenericClass<T>>(parameterizedType)
return adapter.toJson(value)
}
For instance you want to convert an object of SomeGenericClass<*> to JSON.
#JsonClass(generateAdapter = true)
class SomeGenericClass<T>(
#Json(name = "pages")
val pages: Int = 0,
)
#JsonClass(generateAdapter = true)
data class Book(
#Json(name = "id")
val id: String,
)
val book = SomeGenericClass<Book>(
pages = 100,
author = "Aaa",
...)
Then you should create a Moshi object and call convert(book).
See also 1 and 2.
I have three data classes, and I want create a generic function to map them:
data class Visits(
val present: List<Present>,
val past: List<Past>
)
data class Present{
val field1: String,
val field2: String
}
data class Past{
val field1: String,
val field2: String
}
The generic function I had tried something like
private fun doSomething(visits: Visits, position: Int) {
when (position) {
0 -> setItems(visits.present)
1 -> setItems(visits.past)
...
}
}
private fun <T> setItems(visits: List<T>): ArrayList<Something> {
val items: ArrayList<Something> = arrayListOf()
visits.forEach { i ->
items.add(
Something(
i.field1,
i.field2
)
)
}
return items
}
Overall thats the code, I've tried to use something like
inline fun <reified T> doSomethingWithType(list: List<T>) {
// do something with visits list, which can be List<Present> or List<Past>
}
I would like to avoid having duplicated code for setItems function for example, setItemsPresent and setItemsPast.
I would suggest to solve this with an interface,
interface Item {
val field1: String
val field2: String
}
data class Present(
override val field1: String,
override val field2: String
) : Item
data class Past(
override val field1: String,
override val field2: String
) : Item
Then your child fragment can take a list of type List<Item> and work whether they're Present or Past data objects.
There's then not much point to a generic map function, but so you know, what you need is more info about the generic through a generic constraint.
data class Something(
val field1: String,
val field2: String
)
fun <T : Item> mapToSomething(visits: List<T>): List<Something> {
return visits.map { Something(it.field1, it.field2) }
}
I read topics like Android GSON deserialize delete empty arrays, https://medium.com/#int02h/custom-deserialization-with-gson-1bab538c0bfa, How do you get GSON to omit null or empty objects and empty arrays and lists?.
For instance, I have a class:
data class ViewProfileResponse(
val success: Int,
val wallets: List<Wallet>?,
val contact: Contact?,
val code: Int,
val errors: ErrorsResponse?
) {
data class Contact(
val countryId: Int,
val regionId: Int,
val cityId: Int
)
data class Wallet(
val id: Int,
val currency: String?,
val createTime: Int,
val balance: Float,
val bonusWallet: BonusWallet?
) {
data class BonusWallet(val id: Int, val balance: Float)
}
data class ErrorsResponse(val common: List<String>?)
class Deserializer : ResponseDeserializable<ViewProfileResponse> {
override fun deserialize(content: String): ViewProfileResponse? =
Gson().fromJson(content, ViewProfileResponse::class.java)
}
}
As you see, I have a complex class with subclasses, any of each can be null. But instead of sending null or {} in these fields, a server sends [] in JSON.
I mean, instead of "contact": null I get "contact": [].
How to write a custom deserializer for Gson? So that empty arrays could be removed, but other types retain. I have tens of those classes.
A temporary solution is based on https://stackoverflow.com/a/54709501/2914140.
In this case Deserializer will look like:
class Deserializer : ResponseDeserializable<ViewProfileResponse> {
private val gson = Gson()
private val gsonConverter = GsonConverter()
override fun deserialize(content: String): ViewProfileResponse? =
gson.fromJson(gsonConverter.cleanJson(content), ViewProfileResponse::class.java)
}
The result of this code is what I want :
override fun onResponse(call:Call<MainResp<ItemMaterial>>,response:Response<MainResp<ItemMaterial>>){
val arawjson: String = Gson().toJson(response.body())
val dataType = object : TypeToken<MainResp<ItemMaterial>>() {}.type
val mainResp: MainResp<ItemMaterial> = Gson().fromJson<MainResp<ItemMaterial>>(arawjson, dataType)
........
}
But when I make it simple class so I can access every function with object data type parameter.
class Convert<T>{
//fungsi umum untuk konversi gson sesuai dengan output datatype sebagai parameter yakni T
fun convertRespByType(response: Response<MainResp<T>>): MainResp<T> {
val arawjson: String = Gson().toJson(response.body())
val dataType = object : TypeToken<MainResp<T>>() {}.type
val mainResp: MainResp<T> = Gson().fromJson<MainResp<T>>(arawjson, dataType)
return mainResp
}
}
And call it :
override fun onResponse(call:Call<MainResp<ItemMaterial>>,response:Response<MainResp<ItemMaterial>>){
val aconvert : Convert <ItemMaterial> = Convert()
val mainResp : MainResp<ItemMaterial> = aconvert.convertRespByType(response)
........
}
But the second result which I call with the class is different with the first one ?
I think the parameter not passing to the class. Can you give me a recommedation ?
Thanks
The problem is type erasure.
In the statement TypeToken<MainResp<T>>() the type T is not available at runtime.
In Kotlin you could use inline reified to solve your problem. Reified does only work with functions and not with classes:
inline fun <reified T> convertRespByType(response: Response<MainResp<T>>): MainResp<T> {
val arawjson: String = Gson().toJson(response.body())
val dataType = object : TypeToken<MainResp<T>>() {}.type
val mainResp: MainResp<T> = Gson().fromJson<MainResp<T>>(arawjson, dataType)
return mainResp
}
Usage:
val mainResp = convertRespByType<ItemMaterial>(response)
I have this entity :
#Entity(tableName = "recipes")
data class Recipe(
#PrimaryKey val id: Int,
val name: String,
#TypeConverters(Converters::class)
val categories: List<Int>,
val picture: String,
#Embedded(prefix = "time")
val time: Time,
val people: Int,
val difficulty: Int,
val price: Int,
#Embedded(prefix = "ingredients")
#TypeConverters(Converters::class)
val ingredients: List<Ingredient>,
#Embedded(prefix = "utensils")
#TypeConverters(Converters::class)
val utensils: List<Utensil>,
#Embedded(prefix = "steps")
#TypeConverters(Converters::class)
val steps: List<Step>,
val createdAt: String,
val updatedAt: String
) : Serializable
And when I compile it, I got
Cannot figure out how to save this field into database. You can consider adding a type converter for it.
private final java.util.List<java.lang.Integer> categories = null;
Problem is, I added the TypeConverter, it's in my class Converters :
#TypeConverter
fun fromIntList(value: List<Int>): String {
val gson = Gson()
val type = object : TypeToken<List<Int>>() {}.type
return gson.toJson(value, type)
}
#TypeConverter
fun toIntList(value: String): List<Int> {
val gson = Gson()
val type = object : TypeToken<List<Int>>() {}.type
return gson.fromJson(value, type)
}
It should work since it knows that it should transforma list of Int to strings.
Futhermore, I got this error which I dont know where it can come from, and I have it like 5 times.
Entities and Pojos must have a usable public constructor. You can have an empty
constructor or a constructor whose parameters match the fields (by name and type).
I added the nested class like Ingredient, Step, Ustensil but I dont post the code since it should not be related.