Serialize Generics via enum using moshi - android

Background
Using Moshi, I want to create generic adapter for enum that points to a class
I want to use enum type because further down the elements i have complex structure which further boils to different types.
Is it possible to serialize this way via via Moshi?
I tried to make a generic adapter that can handle any type in Attempt but so far I only have the clazz object not the actual T.
Sample Json
{
"items": [
{
"type": "A",
"apple": "123 Apples"
},
{
"type": "B",
"organge": "Banana 12",
"info": {}
},
{
"type": "C",
"grapes": "Green",
"quantity": {
"inStock": "12",
"offShelf": "12"
}
}
]
}
Class Structure
classs FruitResponse(val items: List<FruitTypes>)
#JsonClass(generateAdapter = false)
enum class FruitType(val clazz: Class<*>) {
A(Apple::class.java),
B(Banana::class.java),
C(Grapes::class.java)
}
Attempt
class FruitsAdapter<T : Enum<*>>(enumType: Class<T>) : JsonAdapter<T>() {
private val nameStrings: Array<String?>
private val nameConstantMap: MutableMap<String, T>
init {
try {
val constants = enumType.enumConstants
nameStrings = arrayOfNulls<String>(constants?.size ?: 0)
nameConstantMap = LinkedHashMap()
constants?.forEachIndexed { index, constant ->
val annotation = enumType.getField(constant.name).getAnnotation(Json::class.java)
val name = annotation?.name ?: constant.name
nameConstantMap[name] = constant
nameStrings[index] = name
}
} catch (e: NoSuchFieldException) {
throw AssertionError("Missing field in ${enumType.name}")
}
}
#Throws(IOException::class)
override fun fromJson(reader: JsonReader): T {
val name = reader.nextString()
val constant = nameConstantMap[name]
if (constant != null) return constant
throw JsonDataException(
"Expected one of ${Arrays.asList(*nameStrings)} " +
"but was $name at path ${reader.path}"
)
}
#Throws(IOException::class)
override fun toJson(writer: JsonWriter, value: T?) {
val newValue = nameConstantMap.filter { value == it.value }.map { it.key }.firstOrNull()
if (newValue != null) writer.value(newValue) else writer.nullValue()
}
}
// Usage
val moshiAdapter = Moshi.Builder()
.add(
FruitType::class.java,
FruitsAdapter(FruitType::class.java)
).build()

Take a look at PolymorphicJsonAdapterFactory which can be found in moshi-adapters artifact.
There's also moshi-sealed if you want avoid the boilerplate of manually writing a polymorphic adapter.

Related

Consuming Polymorphic Jsons with Retrofit and Kotlin

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

Parse internal map from JSON in Moshi

I am trying to parse following JSON using moshi but I am unable to dynamic data like USA or UK. USA and UK are dynamic keys.
{
"USA": {
"name": "United State of America",
"code": "US"
},
"UK": {
"name": "United Kingdom",
"code": "UK"
},
"soft_update": "500",
"hard_update": "500"
}
Data class:
data class ApiAppUpdate(val countryMap: Map<String, ApiCountry>,
#field:Json(name = "hard_update")
val forceUpdateVersion: Int,
#field:Json(name = "soft_update")
val softUpdateVersion: Int)
Following is my json adapter code:
fun getConfig(){
val adapter: JsonAdapter<ApiAppUpdate> = moshi.adapter(ApiAppUpdatee::class.java)
}
I get soft and hard update values but countryMap is always null. I am not sure what's wrong in here. Can someone please help me. Thanks.
The problem is that your model description would match this json and not the one you attached:
{
"countryMap":{
"USA":{
"name":"United State of America",
"code":"US"
},
"UK":{
"name":"United Kingdom",
"code":"UK"
}
},
"soft_update":"500",
"hard_update":"500"
}
In your example "USA" and "UK" are on the same level with "soft_update" and "hard_update".
UPDATE:
This is a working solution:
data class ApiCountry(val name: String, val code: String)
data class ApiAppUpdate(
val countryMap: Map<String, ApiCountry>,
#field:Json(name = "hard_update")
val forceUpdateVersion: Int,
#field:Json(name = "soft_update")
val softUpdateVersion: Int
)
class ApiUpdateAdapter {
#FromJson
fun fromJson(reader: JsonReader): ApiAppUpdate {
var forceUpdateVersion: Int = -1
var softUpdateVersion: Int = -1
val map: MutableMap<String, ApiCountry> = mutableMapOf()
reader.beginObject()
while (reader.hasNext()) {
when (reader.peek()) {
JsonReader.Token.NAME ->
when (val fieldName = reader.nextName()) {
"hard_update" -> forceUpdateVersion = reader.nextInt()
"soft_update" -> softUpdateVersion = reader.nextInt()
else -> {
reader.beginObject()
var name = ""
var code = ""
while (reader.hasNext()) {
when (reader.nextName()) {
"name" -> name = reader.nextString()
"code" -> code = reader.nextString()
else -> reader.skipValue()
}
}
reader.endObject()
map[fieldName] = ApiCountry(name, code)
}
}
else -> reader.skipValue()
}
}
reader.endObject()
return ApiAppUpdate(map, forceUpdateVersion, softUpdateVersion)
}
}
fun main() {
val sourceJson =
"{\"USA\":{\"name\":\"United States of America\",\"code\":\"US\"},\"UK\":{\"name\":\"United Kingdom\",\"code\":\"UK\"}" +
",\"soft_update\":\"200\",\"hard_update\":\"300\"}"
val adapter = Moshi.Builder()
.add(ApiUpdateAdapter())
.build()
.adapter(ApiAppUpdate::class.java)
val apiAppUpdate = adapter.fromJson(sourceJson)
println(apiAppUpdate)
}

android- how to get list of properties from MutableList

This is my JSON :
{
"cats": [
{
"id": "2",
"uid": "2",
"name": "English",
"date_update": "2019-04-22 15:31:00",
"numCards": 0
}
]
}
I've these two classes:
data class CatModelStr(
val cats: List<Cat>
)
data class Cat(
val date_update: String,
val id: String,
val name: String,
val numCards: Int,
val uid: String
)
I'm using MVVM and android architecture components. This is my model class for getting the data:
class CategoryModel(private val netManager: NetManager) {
var dateChanges: String = "null";
fun getCats(): MutableLiveData<MutableList<CatModelStr>> {
var list = MutableLiveData<MutableList<CatModelStr>>();
if (netManager.isConnected!!) {
list = getCatsOnline();
}
return list
}
private fun getCatsOnline(): MutableLiveData<MutableList<CatModelStr>> {
var list:MutableLiveData<MutableList<CatModelStr>> =MutableLiveData()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(MyApp().uid, dateChanges)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list += success
},{
error->
Log.v("this","ErrorGetCats "+ error.localizedMessage);
}
)
return list;
}
operator fun <T> MutableLiveData<MutableList<T>>.plusAssign(values: List<T>) {
val value = this.value ?: arrayListOf()
value.addAll(values)
this.value = value
}
I have a viewModel and activity for getting the data and it works fine. The problem is this, I want to get cat values (the json properties inside cat) out of my MutableLiveData.
This is my activity code:
vm.getCats().observe(this, Observer {
if(it!=null) {
rc_cats.visibility= View.VISIBLE
pb.visibility=View.GONE
catAdapter.reloadData(it)
}
})
The value is MutableList<CatModelStr> and I need Cat.
How can I get Cat out of MutableList?
What I am getting is that you don't need MutableList list from your response instead you need CatModelStr
you need to make the following changes from starting i guess.
class CategoryModel(private val netManager: NetManager) {
var dateChanges: String = "null";
fun getCats(): MutableLiveData<CatModelStr> {
var list = MutableLiveData<CatModelStr>();
if (netManager.isConnected!!) {
list = getCatsOnline();
}
return list
}
private fun getCatsOnline(): MutableLiveData<CatModelStr> {
var list:MutableLiveData<CatModelStr> = MutableLiveData()
val getCats = ApiConnection.client.create(Category::class.java)
getCats.getCats(MyApp().uid, dateChanges)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ success ->
list.setValue(success)
},{
error->
Log.v("this","ErrorGetCats "+ error.localizedMessage);
list.setValue(null)
}
)
return list;
}
and now if observe this you will get CatModelStr instead of MutableList and the reason is that your JSON is giving you data in format CatModelStr, you are trying to get this unnecessarily in MutableList which also won't work and you can't parse your JSON given the format.

How to get data from JsonObject and set as Array on RecyclerView in Kotlin?

I'm working on a project that was previously done by another developer. I continue to call API for the project. I've followed his way. But, I get stuck when I want to retrieve JsonArray and show it into RecyclerView.
How I can to get the data from JSONArray.
Interface:
interface TripService {
#FormUrlEncoded
#POST("driver/getAvailableTripList")
fun availableTripList(#Field("page") page : String): Call<JsonObject>
}
TripServiceHandler:
class TripServiceHandler {
private var instance: TripService? = null
// Singleton instance to access through the application
init {
if(instance == null) {
instance = TripFactory().createJourney(TripService::class.java)
}
}
// Method for Driver Balance Service
fun availableTripList(page: String, listener: ServerCallbackListener) =
this.instance?.let {this.instance!!.availableTripList(page).enqueue(RetrofitCallBackHandler.getHandler(listener))
}
Fragment:
private fun getAvailableTrip(){
showLoading()
TripServiceHandler().availableTripList("1", availableTripHandler)
}
private var availableTripHandler: ServerCallbackListener = object : ServerCallbackListener {
override fun onSuccess(baseResponse: JsonObject?) {
hideLoading()
val data = baseResponse!!.getAsJsonObject(AppConstants.KEY_DATA)
}
override fun onFailure(message: String?) {
showMessageBar(message, SnackBarUtility.MessageType.ERROR)
}
}
Data Model:
class Trip : Serializable {
#SerializedName("tid")
var tripId: String? = null
#SerializedName("max_passengers")
var maxPass: String? = null
#SerializedName("driver_revenue")
var priceTrip: String? = null
#SerializedName("origin_coordinate")
var originCoordinate: List<Coordinate>? = null
#SerializedName("destination_coordinate")
var destCoordinate: List<Coordinate>? = null
}
the jsonArray
"rows": [
{
"tid": "44551",
"did": null,
"status": 1,
"leaving_time": "1547093186",
"max_passengers": 12,
"total_revenue": 0,
"driver_revenue": 0,
"origin_coordinate": {
"x": 1.43762623,
"y": 103.80153311
},
"destination_coordinate": {
"x": 1.29481854,
"y": 103.78735487
},
"total_requests": 12,
"destination_geom": "0101000020E610000048F0284063F25940854F5F1901BFF43F",
"origin_geom": "0101000020E61000002AB6CC40C7F25940032A19ECF7E4F63F",
"stops": [
{
"sid": "46969",
"count": "4",
"order_seq": 1,
"mode": 0,
"name": "Woodlands Ave 4",
"description": "Blk 532",
"coordinate": {
"x": 1.43059181782,
"y": 103.792699721
},
"eta_time": "1547093186",
"leaving_time": "1547093366"
},
As I have seen, your data should be a JSONobject type value already and since you are using serialized name i assume you are using Gson library for it.
First, make another model beforehand to contain your List (since it passed in rows)
data class TripList:Serializeable{
#("rows")
myList:List<Trip> = ArrayList()
}
Then, you can try this
val tripList = gson.fromJson(data, TripList::class.java)
Your tripList will then be TripList type of class, access your list with tripList.myList. Hope it will work for your solution

Android and Moshi Adapter with generic type

I'm trying to use moshi with my Android project, but I am running into some problems.
Here is a stripped down sample JSON
{
"data": [
{
"label": "May",
"schedule_items": [
{
"type": "event",
"item": {
"foo": "bar",
"some_prop": 1
},
"schedule_item_groups": [
{
"label": "Friday May 4th",
"schedule_items": [
{
"type": "check_in",
"item": {
"a_different_prop": 15
},
"schedule_item_groups": null
},
{
"type": "game",
"item": {
"yet_another_different_prop": 3598
},
"schedule_item_groups": null
}
]
}
]
}
]
}
]
}
As you can see, it is a list of ScheduleGroups, and within that object you have a label and schedule_items. Which is an array of ScheduleItem with 3 fields:
type: String label to identify which type of Item this is
item: Can be of class Event, Game, and CheckIn
schedule_item_groups: A ScheduleGroup which is a list of more -
ScheduleItems
So the first problem is a ScheduleGroup has a list of ScheduleItems and each item can have it's own list of ScheduleGroup containing more items.
The second problem is the item field, it needs to be instantiated as one of three classes: Event, Game, CheckIn.
I've been working at it for awhile, and so far I can only get one working at a time, but not both.
Here are the data classes (I have only included one of the Item classes):
data class ScheduleGroup(
val label: String,
val schedule_items: List<ScheduleItem<Any>>
)
data class ScheduleItem<out T>(
val type: String,
val schedule_item_groups: List<ScheduleGroup>
val item: T
) {
abstract class Item
}
data class Event(
val some_prop: Int,
val foo: String
) : ScheduleItem.Item()
This is how I got the dynamic Generic class Item to work:
#FromJson
fun fromJson(map: Map<*, *>): ScheduleItem<Any> {
val moshi: Moshi = Moshi.Builder().build()
val type: String = map["type"] as String
val itemJson = moshi.adapter(Map::class.java).toJson(map["item"] as Map<*, *>)
val item = when (type) {
EventType.EVENT -> moshi.adapter(Event::class.java).fromJson(itemJson)
EventType.GAME -> moshi.adapter(Game::class.java).fromJson(itemJson)
EventType.CHECK_IN, EventType.CHECK_OUT ->
moshi.adapter(CheckIn::class.java).fromJson(itemJson)
else -> throw Error("Unknown type was found $type")
}
val scheduleGroupType = Types.newParameterizedType(List::class.java, ScheduleGroup::class.java)
#Suppress("UNCHECKED_CAST")
val scheduleGroupJson = moshi.adapter<List<ScheduleGroup>>(scheduleGroupType)
.toJson(map["schedule_item_groups"] as List<ScheduleGroup>?)
val list: List<ScheduleGroup>? = moshi
.adapter<List<ScheduleGroup>>(scheduleGroupType).fromJson(scheduleGroupJson)
return ScheduleItem(type, list ?: listOf(), item)
}
It will correctly create the right Item class, but when I try to add the List<ScheduleGroup> I get errors, and no matter what I do I cannot seem to get both to work.
Edit:
I Have updated the code to show what I'm using to try and deserialize the schedule_item_groups which is a List<ScheduleGroup>.
I get an error: (This is a different error than I got before...)
com.squareup.moshi.JsonDataException: java.lang.IllegalArgumentException: Can not set final java.lang.String field com.roomroster.mobile_android.data.api.schedule.models.ScheduleGroup.label to com.squareup.moshi.LinkedHashTreeMap at $.data[0].schedule_items[1]
I figured this out awhile ago, but I guess I can post what I did in case it will help anyone in the future.
First I created a temporary intermediatry class, to hold the data before the generic data was made.
data class ScheduleItem<T>(
val date: Date,
val type: String,
var scheduleGroups: List<ScheduleGroup> = listOf(),
var item: T
) {
data class ScheduleItemJson(
val date: Date,
val type: String,
val schedule_item_groups: List<ScheduleGroup>? = listOf(),
val item: Any
)
}
Then in the adapter
#FromJson fun fromJson(item: ScheduleItem.ScheduleItemJson): ScheduleItem<Any> {
val moshi: Moshi = Moshi.Builder().build()
val json = moshi.adapter(Map::class.java).toJson(item.item as Map<*, *>)
return ScheduleItem(
item.date,
item.type,
item.schedule_item_groups ?: listOf(),
when (item.type) {
ItemType.GAME -> moshi.adapter(GameItem::class.java).fromJson(json)
ItemType.EVENT -> moshi.adapter(EventItem::class.java).fromJson(json)
ItemType.CHECK_IN, ItemType.CHECK_OUT ->
moshi.adapter(ReservationItem::class.java)
.fromJson(json).apply { this!!.type = item.type }
else -> ScheduleItem.NullItem()
}!!
)
}
The when statement is what creates the <T : Item> and passes it to the ScheduleItem constructor.
Recently I went through similar problem, I made use of sealed class and JsonAdapter to build models dynamically.
I posted my answer in another post, see https://stackoverflow.com/a/56897476/5584709

Categories

Resources