I have an object that comes down from the API with another json object (with no named attributes) as one of its attributes:
"stickerData": {}
I would like this to be parsed into this object:
#JsonClass(generateAdapter = true)
class StickerDto(
#Json (name = "totalAnimatedStickers") val total: Int,
#Json(name = "pages") val pages: Int,
#Json(name = "data") val stickers: List<Sticker>
)
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: String,
)
The architecture of this app uses a single Retrofit instance for every API call:
private fun createNewUserApiClient(authRefreshClient: AuthRefreshClient,
preferencesInteractor: PreferencesInteractor): UserApiClient {
val moshi = Moshi.Builder()
.add(SkipBadElementsListAdapter.Factory)
return Retrofit.Builder()
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(Interactors.apiEndpoint)
.build()
.create(UserApiClient::class.java)
}
Which, uses this adapter that you can see getting attached above:
internal class SkipBadElementsListAdapter(private val elementAdapter: JsonAdapter<Any?>) : JsonAdapter<List<Any?>>() {
object Factory : JsonAdapter.Factory {
override fun fromJson(reader: JsonReader): List<Any?>? {
val result = mutableListOf<Any?>()
reader.beginArray()
while (reader.hasNext()) {
try {
val peeked = reader.peekJson()
result.add(elementAdapter.fromJson(peeked))
} catch (e: JsonDataException) {
Timber.w(e, "Item skipped while parsing:")
}
reader.skipValue()
}
reader.endArray()
return result
}
}
However, this adapter does not allow for the parsing of a JSON object as a string. If I try, it throws a
Gson: Expected a string but was BEGIN_OBJECT
error. Is there any way to get this adapter to parse attributes like this as raw strings, rather than looking for an object ?
The stickerData should be Object in POJO class, like this...
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: StickerData,
)
I have a Sticker class and its wrapper:
#JsonClass(generateAdapter = true)
class StickerDto(
#Json (name = "totalAnimatedStickers") val total: Int,
#Json(name = "pages") val pages: Int,
#Json(name = "data") val stickers: List<Sticker>
)
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: JsonObject,
var isSelected:Boolean = false
)
The stickerData attribute comes from the api with a dynamic json object with unknown attributes
"stickerData": {}
How do I deserialize an object like that using Moshi?
My current retrofit client:
private fun createNewFriendsClient(authRefreshClient: AuthRefreshClient,
preferencesInteractor: PreferencesInteractor): FriendsApiClient {
val logger = run {
val httpLoggingInterceptor = HttpLoggingInterceptor()
httpLoggingInterceptor.apply {
httpLoggingInterceptor.level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
}
val okHttp = OkHttpClient.Builder().addInterceptor(logger).authenticator(RefreshUserAuthenticator(authRefreshClient, preferencesInteractor,
UnauthorizedNavigator(SDKInternal.appContext, Interactors.preferences))).build()
return Retrofit.Builder()
.client(okHttp)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create())
.baseUrl(Interactors.apiEndpoint)
.build()
.create(FriendsApiClient::class.java)
}
Gives me an
"Unable to create converter for class StickerDto"
Caused by NoJsonAdapter for java.util.Comparator<? super java.lang.String>
error. What converter do I need to use if not that Moshi one? Trying to pull it down as a string also gives an error as it is expecting and object. I just need that string.
Edit, the Json string is very long but it begins like this:
{"tileId":"1264373a-24d8-4c10-ae90-d6e8f671410c","friendId":"2c50f187-039a-4f85-b12b-0c802396a611","name":"David Carey","message":"Joined WeAre8","animatedSticker":{"v":"5.5.7","fr":24,"ip":0,"op":48,"w":1024,"h":1024,"nm":"party_popper","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"C | Position","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[176,892,0],"to":[-6.667,6.667,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":7,"s":[136,932,0],"to":[0,0,0],"ti":[-6.667,6.667,0]},{"t":11,"s":[176,892,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":7,"s":[115,75,100]},{"i":{"x":[0,0,0.667],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0]},"t":11,"s":[95,105,100]},{"t":20,"s":[100,100,100]}],"ix":6}},"ao":0,"ef":[{"ty":5,"nm":"Controller","np":13,"mn":"Pseudo/DUIK controller","ix":1,"en":1,"ef":[{"ty":6,"nm":"Icon","mn":"Pseudo/DUIK controller-0001","ix":1,"v":0},{"ty":2,"nm":"Color","mn":"Pseudo/DUIK controller-0002","ix":2,"v":{"a":0,"k":[0.92549020052,0.0941176489,0.0941176489,1],"ix":2}},{"ty":3,"nm":"Position","mn":"Pseudo/DUIK controller-0003","ix":3,"v":{"a":0,"k":[0,0],"ix":3}},{"ty":0,"nm":"Size","mn":"Pseudo/DUIK controller-0004","ix":4,"v":{"a":0,"k":100,"ix":4}},{"ty":0,"nm":"Orientation","mn":"Pseudo/DUIK controller-0005
Note that JsonObject is a class from the gson package, so if you want to use Moshi you will need to switch to JSONObject which is the default class supported by Android.
To do this you will need to write your own JSONObject adapter.
First, write your adapter class:
import com.squareup.moshi.FromJson
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
import okio.Buffer
import org.json.JSONException
import org.json.JSONObject
class JSONObjectAdapter {
#FromJson
fun fromJson(reader: JsonReader): JSONObject? {
// Here we're expecting the JSON object, it is processed as Map<String, Any> by Moshi
return (reader.readJsonValue() as? Map<String, Any>)?.let { data ->
try {
JSONObject(data)
} catch (e: JSONException) {
// Handle exception
return null
}
}
}
#ToJson
fun toJson(writer: JsonWriter, value: JSONObject?) {
if (value != null) {
writer.value(Buffer().writeUtf8(value.toString()))
} else {
writer.value(null as String?)
}
}
}
Adjust your retrofit build to provide custom Moshi object when creating the MoshiConverterFactory:
.addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().add(JSONObjectAdapter()).build()))
and then you are good to go and use JSONObject
#Json(name = "stickerData") val stickerData: JSONObject
Good luck and I hope this helps!
Built-in Type Adapters for Moshi include Arrays, Collections, Lists, Sets, and Maps. A JsonObject type is not provided with Moshi itself, but it would be an enhanced Map<String, Any> anyhow, so just use the Map instead of an object.
#JsonClass(generateAdapter = true)
class Sticker(
#Json(name = "name") val name: String,
#Json(name = "id") val id: String,
#Json(name = "stickerData") val stickerData: Map<String, Any>,
var isSelected: Boolean = false
)
The values are automatically converted as well. Thus you'll find strings, lists or numbers there.
I made stickerData into a Map and used a GsonConverterFactory instead of Moshi.
I am using Retrofit for API Calls.
I was able to cache data class which contains only strings and int.
But now I want to cache another model class which has nested model classes like
below.
#Entity data class ConsumptionDashboardResponse(
#Id(assignable = true)
var id: Long? = null,
#Transient
#SerializedName("data")
#NameInDb("data_consumption")
var `data`: DataConsumption,
#SerializedName("responseCode")
val responseCode: Int,
#SerializedName("responseDesc")
val responseDesc: String,
#SerializedName("sessionLang")
val sessionLang: String )
DataConsumption Class:
#Entity
data class DataConsumption(
#Id(assignable = true)
var id: Long? = null,
#Backlink
#Transient
#SerializedName("listDivisions")
var listDivisions: List<Divisions>
)
And Divisions Class
#Entity
data class Divisions(
#Id(assignable = true)
var id: Long? = null,
#SerializedName("aggregateBalance")
val aggregateBalance: String,
#SerializedName("aggregateConsumption")
val aggregateConsumption: String,
#SerializedName("division")
val division: String,
#Backlink
#Transient
#SerializedName("listConsumption")
var listConsumption: List<Consumption>,
#SerializedName("unit")
val unit: String
)
Divison class contains list of Consumption
#Entity
data class Consumption(
#Id(assignable = true)
var id: Long? = null,
#SerializedName("aggregateConsumption")
val aggregateConsumption: String,
#SerializedName("billingPeriod")
val billingPeriod: String
)
I am unable to find if I need any type of custom converters or what else I need to do.
If I try to read saved data from ObjectBox this is what I get:
ConsumptionDashboardResponse(id=4, data=null, responseCode=200, responseDesc=SUCCESS, sessionLang=AR)
A Transient field is not persisted in the database. If you have to store properties of non-primitive type either use relationships or create type converters.
I've recently switched from Gson to Moshi and am having trouble parsing some Json.
{
"access_token": "-LNe2LQ7DQH5Y2zs_W5iUumKuaUE",
"token_type": "bearer",
"device_id": "461f-837e-af5050c92fe9",
"expires_in": 3600,
"scope": "*"
}
And here's the model class:
data class AuthToken(
#Json(name = "access_token") val accessToken: String,
#Json(name = "token_type") val tokenType: String,
#Json(name = "device_id") val deviceId: String,
#Json(name = "expires_in") val expiresIn: Int,
#Json(name = "scope") val scope: String
)
Whenever I switch to using Moshi in my retrofit client, I receive the following error:
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull
I have made the field nullable, however it is always deserialized as null. I've checked my retrofit response and it's (obviously) the same when using either Gson or Moshi. What am I doing wrong?
For some reason, when I explicitly tell the AuthToken class to generate an adapter - I receive no null values.
#JsonClass(generateAdapter = true)
data class AuthToken(
#Json(name = "access_token") val accessToken: String,
#Json(name = "token_type") val tokenType: String,
#Json(name = "device_id") val deviceId: String,
#Json(name = "expires_in") val expiresIn: Int,
#Json(name = "scope") val scope: String
)