Missing label with PolymorphicJsonAdapterFactory - android

I'm trying to use PolymorphicJsonAdapterFactory in order to obtain different types, but am always getting odd exception:
Missing label for test_type
My entity:
#JsonClass(generateAdapter = true)
data class TestResult(
#Json(name = "test_type") val testType: TestType,
...
#Json(name = "session") val session: Session,
...
)
Here is my moshi factory:
val moshiFactory = Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(Session::class.java, "test_type")
.withSubtype(FirstSession::class.java, "first")
.withSubtype(SecondSession::class.java, "second")
)
.build()
Structure of json response:
{
response: [
test_type: "first",
...
]
}

test_type must be a field of session class.
if the test_type can't be inside a session class, then you must declare a class for every variant of TestResult containing the specific Session class as follows:
sealed class TestResultSession(open val testType: String)
#JsonClass(generateAdapter = true)
data class TestResultFirstSession(
#Json(name = "test_type") override val testType: String,
#Json(name = "session") val session: FirstSession
) : TestResultSession(testType)
#JsonClass(generateAdapter = true)
data class TestResultSecondSession(
#Json(name = "test_type") override val testType: String,
#Json(name = "session") val session: SecondSession
) : TestResultSession(testType)
and your moshi polymorphic adapter:
val moshiFactory = Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(TestResultSession::class.java,"test_type")
.withSubtype(TestResultFirstSession::class.java, "first")
.withSubtype(TestResultSecondSession::class.java, "second")
)
.build()
it is always good practise to provide a fallback, so your deserialisation doesn't fail, in case test_type is unknown:
#JsonClass(generateAdapter = true)
data class FallbackTestResult(override val testType: String = "") : TestResultSession(testType)
val moshiFactory = Moshi.Builder()
.add(
PolymorphicJsonAdapterFactory.of(TestResultSession::class.java,"test_type")
.withSubtype(TestResultFirstSession::class.java, "first")
.withSubtype(TestResultSecondSession::class.java, "second")
.withDefaultValue(FallbackTestResult())
)
.build()

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 make post request with retrofit + moshi

I'm trying to make a post with retrofit and moshi but keep getting the error mess
com.squareup.moshi.JsonDataException: Expected BEGIN_OBJECT but was STRING at path $
I can't seem to understand why this is so. This is a sample of the json tested on postman:
{
"customerName": "Name",
"customerPhoneNo": "090000000",
"customerAddress": "Address",
"note": "Please",
"items" : [{
"productUid": "5633e1f1-8b00-46de-b73e-43799245a4e8",
"quantity" : "3"
},{
"ProductUid": "fca3ffb1-0130-4e47-b499-721d046c1e32",
"Quantity" : "5"
},
{
"ProductUid": "6a7f3e24-03ff-408a-b67e-8530d411390c",
"Quantity" : "2"
}]
}
My data classes are set up like so:
#Parcelize
data class Order(
val items: List<Item>?,
val customerName: String,
val customerPhoneNo: String,
val customerAddress: String,
val note: String
) : Parcelable
and
#Parcelize
data class Item(
val productUid: String,
var quantity: Int
) : Parcelable
Service utils look like:
interface ProductService {
#Headers("Content-Type: application/json")
#POST("/api/order/saveorder")
suspend fun postProds(#Body order: Order
): 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 sendOrder function is set up like so:
suspend fun sendOrder(order: Order) {
withContext(Dispatchers.Main){
try {
val orderResponse = Network.ProdsApi.retrofitService.postProds(
order )
}
catch (e: Exception) {
Timber.e(e)
}
}
}
The GET request works perfectly.
Any help on this would be appreciated please.
In your Item Data Class you are using quantity as an Int but if you see the Postman JSON response it is a String.
So your class should be like:
data class Item(
#Json(name = "productUid")
val productUid: String?,
#Json(name = "quantity")
var quantity: String
)
Also as I see the key in your JSON response are written in two different ways.
For example your "Product ID" is written as "productUid" in one of the object and is written as "ProductUid" in another object.
So your complete Item Data Class should more look like this :
data class Item(
#Json(name = "productUid")
val productUid: String?,
#Json(name = "ProductUid")
val productUid: String?,
#Json(name = "quantity")
val quantity: String?,
#Json(name = "Quantity")
val quantity: String?
)
Add to app/build.gradle
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.13.0"
Refactor your data class
check the key in your item and replace with right one
if productUid or ProductUid
quantity or Quantity
#JsonClass(generateAdapter = true)
data class Item(
#Json(name = "productUid")
val productUid: String,
#Json(name = "quantity")
var quantity: String
)
#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
)
and try it again

Unable to create #Body converter for class in Retrofit

I have an instance of retrofit built like this
val moshi = Moshi.Builder()
.add(SkipBadElementsListAdapter.Factory)
.add(KotlinJsonAdapterFactory())
.add(Date::class.java, MoshiDateAdapter())
.build()
val okHttpClient = createHttpClientBuilder()
.build()
return Retrofit.Builder()
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(Interactors.apiEndpoint)
.build()
.create(UserApiClient::class.java)
I'm sending an List of this object
internal open class QuizAnswerDto(
#Json(name = "questionOrder") val questionOrder: Int?,
#Json(name = "questionKind") val type: String?,
#Json(name = "questionId") val questionId: String?,
#Json(name = "response") val response: Any?,
#Json(name = "order") val answerOrder: Int?,
#Json(name = "text") val answerText: String?,
#Json(name = "responses") val answersMap: Map<Int, String>?){
companion object {
const val ANGRY_ID = 0
const val UPSET_ID = 1
const val NEUTRAL_ID = 2
const val SATISFIED_ID = 3
const val HAPPY_ID = 4
const val UNKNOWN = -1
const val LIKE_DISLIKE= "yes_no"
const val SENTIMENT ="viewer_sentiment"
const val SINGLE_ANSWER="multiple_choice"
const val MULTIPLE_ANSWERS="select_all_that_apply"
const val SHORT_ANSWER="short_answer"
}
}
With this API call
#POST("campaigns/influencer/sponsorships/watchandrespond/{influencerSponsorshipId}/answers")
#JvmSuppressWildcards
fun submitAnswers(#Path("influencerSponsorshipId") influencerSponsorshipId: String,
#Body request: List<QuizAnswerDto>): Completable
When I do, I get this error:
java.lang.IllegalArgumentException: Unable to create #Body converter
for java.util.List<com.weare8.android.data.quiz.QuizAnswerDto>
(parameter #2)
Caused by: java.lang.IllegalArgumentException: No JsonAdapter for E (with no annotations)
Parameter #2 (questionKind) is always one of the const strings in the companion object, I have no idea what "type variable or wildcard" it is talking about. What am I doing wrong?
From the moshi documentation
val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()
KotlinJsonAdapterFactory should be added as last in the builder. This may solve your problem.

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,
)

Json parsing with moshi

Can anyone please tell me why this is not working
Model class :
#JsonClass(generateAdapter = true)
data class InstagramBusinessAccountResponse(
val data : List<Account>
) {
data class Account(
#Json(name = "id") val id : String,
#Json(name = "instagram_business_account") val instagramBusinessAccount : InstagramBusinessAccount
) {
data class InstagramBusinessAccount(
#Json(name = "id") val id: String,
#Json(name = "name") val name: String,
#Json(name = "profile_picture_url") val profilePictureUrl: String = ""
)
}
companion object {
fun fromJson(json: String) : InstagramBusinessAccountResponse {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(InstagramBusinessAccountResponse::class.java)
return jsonAdapter.fromJson(json)!!
}
}
}
When parsing the following json
{"data":[{"instagram_business_account":{"id":"id","username":"name","name":"Suyash Chavan","profile_picture_url":"image"},"id":"id"}]}
with
InstagramBusinessAccountResponse.fromJson(json.toString())
...
companion object {
fun fromJson(json: String) : InstagramBusinessAccountResponse {
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(InstagramBusinessAccountResponse::class.java)
return jsonAdapter.fromJson(json)!!
}
}
gives instagramBusinessAccount null but if I don't use Custom field names with #Json i.e. replacing instagramBusinessAccount with instagram_business_account and profilePictureUrl with profile_picture_url, it works fine.
I was missing .add(KotlinJsonAdapterFactory()) in Moshi Builder.
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
It works now.

Categories

Resources