I am trying to retrieve several rows from an API using Retrofit and Moshi, but am facing this error:
Retrofit error:- Expected BEGIN_ARRAY but was BEGIN_OBJECT at path$
The API endpoint I am requesting the data from is:
https://thecodecafe.in/gogrocer-ver2.0/api/top_selling
This is the setup code for Retrofit and Moshi that I am using to request the data from the API:
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL = "https://thecodecafe.in/gogrocer-ver2.0/api/"
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface GroceryApiServices {
#GET("top_selling")
fun getProperties():
Call<List<GroceryProperty>>
}
object GroceryApi {
val retrofitServices: GroceryApiServices by lazy { retrofit.create(GroceryApiServices::class.java)}
}
This is the logic of my view model class, showing how I want to retrieve the data:
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kotlin_developer.grocerysell.network.GroceryApi
import com.kotlin_developer.grocerysell.network.GroceryProperty
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel: ViewModel() {
private val _response = MutableLiveData<String>()
val response: LiveData<String>
get() = _response
init {
getGroceryProperties()
}
private fun getGroceryProperties(){
GroceryApi.retrofitServices.getProperties().enqueue(object : Callback<List<GroceryProperty>>{
override fun onFailure(call: Call<List<GroceryProperty>>, t: Throwable) {
_response.value = t.message
}
override fun onResponse(
call: Call<List<GroceryProperty>>,
response: Response<List<GroceryProperty>>
) {
_response.value="Success ${response.body()?.size} Grocery Property arrived"
}
})
}
override fun onCleared() {
super.onCleared()
}
}
This is how I want to retrieve data in my view model class
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.kotlin_developer.grocerysell.network.GroceryApi
import com.kotlin_developer.grocerysell.network.GroceryProperty
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class OverviewViewModel: ViewModel() {
private val _response = MutableLiveData<String>()
val response: LiveData<String>
get() = _response
init {
getGroceryProperties()
}
private fun getGroceryProperties(){
GroceryApi.retrofitServices.getProperties().enqueue(object : Callback<List<GroceryProperty>>{
override fun onFailure(call: Call<List<GroceryProperty>>, t: Throwable) {
_response.value = t.message
}
override fun onResponse(
call: Call<List<GroceryProperty>>,
response: Response<List<GroceryProperty>>
) {
_response.value="Success ${response.body()?.size} Grocery Property arrived"
}
})
}
override fun onCleared() {
super.onCleared()
}
}
The error you getting is Moshi telling you that it expects a JSON array, but it got an object. Your Retrofit endpoint method looks like this:
#GET("top_selling")
fun getProperties():
Call<List<GroceryProperty>>
Here, you are telling Retrofit that you expect a List. In JSON, this would be the array Moshi is expecting. However, upon clicking on the link to the endpoint you provided, the JSON you are receiving looks like this:
{
"status": "1",
"message": "top selling products",
"data": [
...
]
}
As you can see, this JSON is not an array but an object that contains an array, and that's where Moshi's error originates. To deserialize it into a List, it expected the begin of an array ([), but what it found was actually the begin of an object ({)
To sum it up, you are not expecting a List, but an object, which in turn contains that List (the data array in the JSON).
You would need to define another class that encapsulates this List, something the likes of this:
data class TopSellingResponse(
val status: String,
val message: String,
val data: List<GroceryProperty>
)
If you then change your method signature to
#GET("top_selling")
fun getProperties():
Call<TopSellingResponse>
Moshi should be able to deserialize the JSON object into your class and the contained data array into the List you initially expected.
Related
I am using an existing API that requires a POST in XML. That XML will contain an API key and a serial number and the response will be in JSON. I am using Android Studio and Kotlin to write the app. I am also trying to use Retrofit2.
My problem is that I can not find how to use both XML and JSON. I know this code is incomplete but I would be interested in a good source of information on these topics. If you have any good lessons please share.
This is my Interface
package com.example.project
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface APIInterface {
#POST("APIEndpont")
fun submitSerNum(#Body serNum: SerialNumber): Call<MyData>
}
This is where I try to just log the JSON as a string
val retrofit = ServiceBuilder.buildService(APIInterface::class.java)
val obj = SerialNumber(ser = "000")
retrofit.submitSerNum(obj).enqueue(
object:Callback<MyData>{
override fun onResponse(call: Call<MyData>, response: Response<MyData>) {
Log.d("TAG", "${response.body().toString()}")
}
override fun onFailure(call: Call<MyData>, t: Throwable) {
Log.d("Tag Failure", "Failure")
}
}
)
This is my service builder
package com.example.project
import android.util.Log
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServiceBuilder {
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
fun <T> buildService(service: Class<T>): T {
return retrofit.create(service)
Log.d("Creating Service", "Service Created");
}
}
I have followed a few tutorials on how to do API calls mostly GETs though. I can not figure out how to adapt the code to my needs.
learning about retrofit but couldn't write the tests for it. I came from jest background and struggling to test two things:
that the call was making to a specific end point and its status.
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
interface WeatherAPI {
#GET("current.json")
suspend fun getCurrentWeatherData(#Query("key") apiKey: String, #Query("q") cityName: String, #Query("qui") quiValue: String): Response<ResponseBody>
companion object {
private const val BASE_URL = "http://api.weatherapi.com/v1/"
val instance: WeatherAPI by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(WeatherAPI::class.java)
}
}
}
Test file:
class WeatherAPITest {
#Test
fun WeatherAPI_getCurrentWeather_apiKey_city(){
runBlocking {
val res = WeatherAPI.instance.getCurrentWeatherData("123", "London", "no")
assertThat(res.code()).isEqualTo(200)
}
}
}
it fails because there is no token. How can i mock the actual api call to return say 200 on the test and confirm something like this:
assertThat(url).isEqualTo('http:....?key=123&q=London')
assertThat(responseCode).isEqualTo(200)
I'm stuck at Exception that appears in the title. I checked similar topics, but they were specific cases that didn't apply to my situation. Below you could see my models, retrofit setup and its usage. I tried removing Body Class, Response Class, just to check if they are culprits, but unfortunately those weren't the case. Maybe someone will be able to figure out what I'm doing wrong?
Stack Trace:
java.lang.IllegalArgumentException: No Retrofit annotation found. (parameter #2)
for method ApiService.login
at retrofit2.Utils.methodError(Utils.java:52)
at retrofit2.Utils.methodError(Utils.java:42)
at retrofit2.Utils.parameterError(Utils.java:61)
at retrofit2.RequestFactory$Builder.parseParameter(RequestFactory.java:311)
at retrofit2.RequestFactory$Builder.build(RequestFactory.java:182)
at retrofit2.RequestFactory.parseAnnotations(RequestFactory.java:65)
at retrofit2.ServiceMethod.parseAnnotations(ServiceMethod.java:25)
at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:168)
at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
at java.lang.reflect.Proxy.invoke(Proxy.java:1006)
at $Proxy1.login(Unknown Source)
at com.rudearts.cyber2020.services.NetworkService$login$1.invokeSuspend(NetworkService.kt:17)
at com.rudearts.cyber2020.services.NetworkService$login$1.invoke(Unknown Source:10)
at kotlinx.coroutines.flow.SafeFlow.collect(Builders.kt:56)
at kotlinx.coroutines.flow.internal.ChannelFlowOperatorImpl.flowCollect(ChannelFlow.kt:144)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo$suspendImpl(ChannelFlow.kt:111)
at kotlinx.coroutines.flow.internal.ChannelFlowOperator.collectTo(Unknown Source:0)
at kotlinx.coroutines.flow.internal.ChannelFlow$collectToFun$1.invokeSuspend(ChannelFlow.kt:33)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:738)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Models:
import com.google.gson.annotations.SerializedName
data class LoginRequest(
#SerializedName("pin") val pin:String,
#SerializedName("pushId") val pushId:String)
import com.google.gson.annotations.SerializedName
data class UserJson (
#SerializedName("id") val id:Long,
#SerializedName("name") val name:String?,
#SerializedName("access_rights") val accessRights:String?)
Retrofit Builder
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitBuilder {
private const val BASE_URL = "<url>"
private val client = OkHttpClient.Builder().build()
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
import com.rudearts.cyber2020.model.LoginRequest
import com.rudearts.cyber2020.model.UserJson
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
interface ApiService {
#Headers("Content-Type: application/json")
#POST("login.php")
suspend fun login(#Body request: LoginRequest):Response<UserJson>
}
Usage in other class:
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import java.util.*
class NetworkService {
private val repoService by lazy { RepoService.instance }
private val apiService = RetrofitBuilder.apiService
fun login(pin:String, token:String): Flow<NetworkResult<Boolean>> = flow {
emit(Loading)
try {
val userJson = apiService.login(LoginRequest(pin,token)).body()
userJson?.let {
val user =
User(userJson.id, userJson.name ?: "", emptyList(), userJson.accessRights ?: "")
repoService.user = user
}
emit(NetworkSuccess(userJson != null))
} catch (throwable: Throwable) {
emit(NetworkError(throwable))
}
}
}
It could be that you are using an older version of okhttp / retrofit as you are using suspend function it requires the latest version of both libraries.
Try call the function like this
fun login(LoginRequest(pin,token))
instead of this
fun login(pin:String, token:String)
the exception says the error is on the second parameter
When I was having an Api Key then I used the following below code to extract the Json data from it.
Now I want to fetch Json data from https://api.coingecko.com/api/v3/exchanges and I don't have any Api Key or query to pass.How can I do it using RetroFit?
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import retrofit2.create
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Query
const val BASE_URL = "https://newsapi.org/"
const val API_KEY = "5f60ae62gcbc4bdaa0d15164d7f1275b"
interface NewsInterface {
#GET("v2/top-headlines?apiKey=$API_KEY")
fun getHeadLines(#Query("country")country:String): Call<News>
}
object NewsService {
val newsInstance :NewsInterface
init {
val retrofit: Retrofit= Retrofit.Builder()
.baseUrl(BASE_URL).addConverterFactory(GsonConverterFactory.create()).build()
newsInstance = retrofit.create(NewsInterface::class.java)
}
}
You API fun should be as,
#GET("api/v3/exchanges")
fun getExchanges(): Call<Response>
Currently, I'm decoding Bitmap the following way:
#GET("api/users/get_avatar/{userId}/default.png")
fun getAvatar(#Header("ApiToken") apiToken: String, #Path("userId") userId: String): Observable<ResponseBody>
and decoding it in ViewModel
val avatar = it()?.let { body ->
val stream = body.byteStream()
BitmapFactory.decodeStream(stream)
}
However, I would like to use for that more elegant Moshi JsonAdapter.
My call looks like:
#GET("api/users/get_avatar/{userId}/default.png")
fun getAvatar(#Header("ApiToken") apiToken: String, #Path("userId") userId: String): Observable<Bitmap>
I'm adding adapter:
return Moshi.Builder()
.add(BitmapAdapter())
However, most probably my adapter is wrong:
private class BitmapAdapter {
#ToJson
fun toJson(value: Bitmap): String {
return value.encodeBase64()
}
#FromJson
fun fromJson(value: String): Bitmap {
return value.decodeBase64()
}
}
How it should look like?
Moshi is meant for parsing JSON, not directly decoding images. If you want to get a Bitmap from a Retrofit client, you'd want a Converter.Factory to supply directly to Retrofit.
Example:
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
class BitmapConverterFactory : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
return if (type == Bitmap::class.java) {
Converter<ResponseBody, Bitmap> {
value -> BitmapFactory.decodeStream(value.byteStream())
}
} else {
null
}
}
}
And supply it wherever you instantiate your Retrofit instance:
Retrofit.Builder()
.baseUrl("https://myapi.com")
.addConverterFactory(BitmapConverterFactory())
.addConverterFactory(MoshiConverterFactory.create())
.build()
Edit: I initially made a mistake in BitmapCoverterFactory. The comparison of type was initially against Bitmap::javaClass, it should be Bitmap::class.java.