How to deserialize raw JSON objects with Moshi/Retrofit - android
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.
Related
What is the right way to post with retrofit 2 and moshi
I've been trying to make a POST with Retrofit 2 and Moshi but I've been unable to get it to work. My data classes look like this: data class Order( val items: List<Item>?, val customerName: String, val customerPhoneNo: String, val customerAddress: String, val note: String ) data class Item( val productUid: String, var quantity: Int ) The interface looks like this interface ProductService { #POST("/api/order/saveorder") suspend fun postProds( #Field("customerName") customerName: String, #Field("customerPhoneNo") customerPhone: String, #Field("customerAddress") address: String, #Field("note") customerNote:String, #Field("items") orderItems: List<Item> ): Response<Order> #GET("/api/product/allproducts") suspend fun getProds(): Response<List<ProdsItem>> } private val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build() object Network { private val retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(MoshiConverterFactory.create(moshi) .asLenient() ) .build() object ProdsApi { val retrofitService: ProductService by lazy { retrofit.create(ProductService::class.java) } } } The postProds fun is called like this: suspend fun sendOrder(order: Order) { withContext(Dispatchers.Main){ try { val orderResponse = Network.ProdsApi.retrofitService.postProds( order.customerName, order.customerPhoneNo, order.customerAddress, order.note, order.items ) } catch (e: Exception) { Timber.e(e) } } } Trying to POST this way keeps yielding this response: Response{protocol=h2, code=400, message=, url= However, I tried converting the Order object to json directly in my viewModel as follows: val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter: JsonAdapter<Order> = moshi.adapter(Order::class.java) val jsonString = jsonAdapter.toJson(customerOrder) Timber.d(jsonString) I then tested the generated jsonString on Postman and got a 200 response. I need some help figuring out what I'm doing wrong in my code, please.
In postman, you are sending data in the body of the request. But in your code, it is going as key-value params. Try to send it in the body from your code. Try below snippet. Update your Order Data class: #JsonClass(generateAdapter = true) data class Order( #Json(name = "items") val items: List<Item>?, #Json(name = "customerName") val customerName: String, #Json(name = "customerPhoneNo") val customerPhoneNo: String, #Json(name = "customerAddress") val customerAddress: String, #Json(name = "note") val note: String ) #JsonClass(generateAdapter = true) data class Item( #Json(name = "productUid") val productUid: String, #Json(name = "quantity") var quantity: Int ) Now the ProductService Interface: interface ProductService { #POST("/api/order/saveorder") suspend fun postProds( #Body request: Order ): Response<Order> #GET("/api/product/allproducts") suspend fun getProds(): Response<List<ProdsItem>> } Now Pass the request object in your function call: suspend fun sendOrder(order: Order) { withContext(Dispatchers.Main){ try { val orderResponse = Network.ProdsApi.retrofitService.postProds(order) } catch (e: Exception) { Timber.e(e) } } }
How to Parse Kotlin Units with Moshi
I'm using retrofit for web requests and then moshi for JSON parsing,this is api #POST("/verify_code/send_email") suspend fun sendEmail(#Body sendEmailRequest: SendEmailRequest): BaseResponse<Unit> the BaseResponse #JsonClass(generateAdapter = true) open class BaseResponse<T> { #Json(name = "code") var code: Int? = null #Json(name = "message") var message: String? = null #Json(name = "data") var data: T? = null } JSON String { "code": 200, "message": "Some Message", "data": null } and error log 2021-11-26 09:59:24.166 14288-14288/com.gow E/FlowKtxKt$next: java.lang.IllegalArgumentException: Unable to create converter for com.gow.base.BaseResponse<kotlin.Unit> for method VerifyApi.sendEmail I tried adding the following, but it didn't work object UnitConverterFactory : Converter.Factory() { override fun responseBodyConverter( type: Type, annotations: Array<out Annotation>, retrofit: Retrofit ): Converter<ResponseBody, *>? { return if (type == Unit::class.java) UnitConverter else null } private object UnitConverter : Converter<ResponseBody, Unit> { override fun convert(value: ResponseBody) { value.close() } } }
it`s the Moshi bug. I solved my problem by using Any instead of Unit. like this: #POST("/verify_code/send_email") suspend fun sendEmail(#Body sendEmailRequest: SendEmailRequest): BaseResponse<Any>
I had the same issue. I fixed it by following the comment on this link. I do not see how you are adding your UnitConverterFactory but in my case, the order you add it is important. I am also using MoshiConverterFactory and ApiResultConverterFactory from EitherNet, so my UnitConverterFactory had to be placed after ApiResultConverterFactory and before MoshiConverterFactory: Retrofit.Builder() ... .addCallAdapterFactory(ApiResultCallAdapterFactory) //the order of the converters matters. .addConverterFactory(ApiResultConverterFactory) .addConverterFactory(UnitConverterFactory) .addConverterFactory(MoshiConverterFactory.create(moshi)) .build()
Moshi serialize generic classes "Failed to find the generated JsonAdapter constructor..."
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.
Kotlin data class converted with GSON always returns null
I try to store a data class using GSON as JSON string. However, gson always returns null: fun jsonTest() { data class ImageEntry( val id: Int, val type: String, val path: String) val images = mutableListOf<ImageEntry>() images.add(ImageEntry(0, "test", "test")) Log.d("EXPORT_TEST", "$images") val gson = Gson() val jsonString = gson.toJson(images) Log.d("EXPORT_TEST", jsonString) } This returns D/EXPORT_TEST: [ImageEntry(id=0, type=test, path=test)] D/EXPORT_TEST: [null] I tried using #SerializedName from here (Kotlin Data Class from Json using GSON) but same result. gson 2.8.7 kotlin 1.5.10 What am I doing wrong?
I am not sure about the true "root" cause here, but this is happening because your ImageEntry class definition is local to the jsonTest() function. Move the class definition outside of the function: data class ImageEntry( val id: Int, val type: String, val path: String ) fun jsonTest() { val images = mutableListOf<ImageEntry>() images.add(ImageEntry(0, "test", "test")) Log.d("EXPORT_TEST", "$images") val gson = Gson() val jsonString = gson.toJson(images) Log.d("EXPORT_TEST", jsonString) } When I run this, the output appears as expected: D/EXPORT_TEST: [{"id":0,"type":"test","path":"test"}]
Get JSONObject as string with moshi/retrofit
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, )