i'm currently working on some basic app which i try to get
an response from API - List of objects.
My data classes Are:
#JsonClass(generateAdapter = true)
data class Tag(
#Json(name = "id")
val id: Int,
#Json(name = "name")
val name: String
)
#JsonClass(generateAdapter = true)
data class Test(
#Json(name = "count")
val count: Int,
#Json(name = "next")
val next: Int,
#Json(name = "previous")
val previous: Int,
#Json(name = "results")
val results: List<Tag>
)
My retrofit build code is:
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
return Retrofit.Builder()
.baseUrl(SERVER_BASE_URL)
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
And my request is very simple:
#GET("api/tags")
suspend fun getTags(): Deferred<Test>
But when i am calling getTags() i get the following error:
java.lang.IllegalArgumentException: Unable to create converter for kotlinx.coroutines.Deferred<com.example.kotlin_ex2.models.Test>
Caused by: java.lang.IllegalArgumentException: No JsonAdapter for kotlinx.coroutines.Deferred<com.example.kotlin_ex2.models.Test> (with no annotations)
Already tried many other ways with no success, what could be the problem?
Thank you
It is because you are using both suspend and Deferred in one function. Convert
#GET("api/tags")
suspend fun getTags(): Deferred<Test>
to
#GET("api/tags")
fun getTags(): Deferred<Test>
Solved.
As i can see, i just needed to remove the Deferred class and write it that way:
#GET("api/tags")
suspend fun getTags(): Test
and no need to call await() from inside the Coroutine.
Related
I'm new to web APIs and retrofit. I'm interacting with TheCatApi and having trouble fetching data that I posted. The API has a list of images and a feature to choose several as favorites with POST requests and retrieve a list of favorites.
My main issue probably has to do with the data class (I have 2):
-For the Image GET request
data class CatPhoto (
#Json(name = "id")
val id: String,
#Json(name = "url")
val imgSrcUrl: String,
#Json(name = "breeds")
val breeds: List<Any>,
#Json(name = "width")
val width: Int,
#Json(name = "height")
val height: Int
)
#GET("$API_V/favourites?limit=100")
suspend fun getMyFavorites(
#Query("sub_id") subId: String
): List<CatPhoto>
-To make an image a favorite using a POST request
data class FavouriteImage (
val image_id: String,
val sub_id: String,
)
#POST("$API_V/favourites?limit=100")
suspend fun addFavorite(
#Body favouriteImage: FavouriteImage
)
This is the error after I POST a favourite and try to retrieve a list of posted favourites:
com.squareup.moshi.JsonDataException: Required value 'imgSrcUrl' (JSON name 'url') missing at $[.1]
It looks like it's expecting imgSrcUrl attribute on the FavouriteImage data class, which makes me think I shouldn't even have the FavouriteImage class. But then how do I make the post request that requires image_id and sub_id in the body?
Here's how I set up the database in the API service file:
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
object CatsApi {
val catsApiService : CatsApiService by lazy {
retrofit.create(CatsApiService::class.java)
}
}
backend of the github project
Edit: As #extremeoats wrote in comments, the api doesn't support passing urls an image indentifier and you have to save its id also for the operations like making favorite etc.
Old answer
Could you please add some code of how a request is made?
It's a local error of Moshi trying to parse the response and not seing the required field (maybe got an error from server - the data structure of an error would be different from a normal response)
I've built a sample app to test this and get a proper response when marking the image as a fav. You are right to use the structure with an image_id and sub_id. Here are some code parts if it helps
Set up the Retrofit (interceptor for debug only, so you can see what exactly you sent and got back)
private val interceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
private val retrofit = Retrofit.Builder()
.baseUrl(ServerApi.BASE_URL)
.client(
OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
)
.addConverterFactory(MoshiConverterFactory.create())
.build()
private val api = retrofit.create(ServerApi::class.java)
1.1. A dependency for the logging interceptor and OkHttp
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0'
Api interface
interface ServerApi {
companion object {
const val API_KEY: String = **PASTE YOUR API KEY HERE**
const val AUTH_HEADER = "x-api-key"
const val BASE_URL = "https://api.thecatapi.com/v1/"
}
#GET("images/search")
fun getImages(#Header(AUTH_HEADER) authHeader: String = API_KEY, #Query("limit") limit: Int = 5): Call<List<ImagesItem>>
#POST("favourites")
fun postFavourite(#Header(AUTH_HEADER) authHeader: String = API_KEY, #Body payload: PostFavouritePayload): Call<String>
}
private var listMyData = Types.newParameterizedType(List::class.java, ImagesItem::class.java)
private val adapter: JsonAdapter<List<ImagesItem>> = Moshi.Builder().build().adapter(listMyData)
Use the api
api.getImages().enqueue(object : Callback<List<ImagesItem>> {
override fun onResponse(call: Call<List<ImagesItem>>, response: Response<List<ImagesItem>>) {
val images = response.body() ?: return
api.postFavourite(payload = PostFavouritePayload(images[0].id, **PASTE ANY STRING HERE AS USER ID**))
.enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) =
Log.d("TestCatApi", "SUCCESS posting a fav: ${response.body()}")
override fun onFailure(call: Call<String>, t: Throwable) =
t.printStackTrace()
})
}
override fun onFailure(call: Call<List<ImagesItem>>, t: Throwable) =
t.printStackTrace()
})
Side notes:
For such example APIs I find very useful a plugin "Json to Kotlin class". Alt+K, then you can paste the String response from a Server or in example, and you have a decent starting point for the data classes (not affiliated with them)
Just in case: you can pass a baseUrl to the Retrofit builder like that
Retrofit.Builder()
.baseUrl(ServerApi.BASE_URL)...
Then you put in #Get, #Post etc. only the part after the "/" of a base url: "images/search"
I practicing on Retrofit with Moshi, in this simple demo app I trying to get list of albums and log it, but I got this exception after running the app
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.anushka.retrofitdemo, PID: 12428
com.squareup.moshi.JsonDataException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at path $
at com.squareup.moshi.JsonUtf8Reader.beginObject(JsonUtf8Reader.java:172)
at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:67)
at com.squareup.moshi.internal.NullSafeJsonAdapter.fromJson(NullSafeJsonAdapter.java:41)
at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:46)
at retrofit2.converter.moshi.MoshiResponseBodyConverter.convert(MoshiResponseBodyConverter.java:27)
at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:243)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:153)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:923)
Data classes
AlbumsItem
#JsonClass(generateAdapter = true)
data class AlbumsItem(
#Json(name = "id")
val id: Int,
#Json(name = "title")
val title: String,
#Json(name = "userId")
val userId: Int
)
Albums List class
#JsonClass(generateAdapter = false)
class Albums : ArrayList<AlbumsItem>()
the service
interface AlbumsService {
#GET("albums")
suspend fun getAlbums(): Response<Albums>
}
The instance of retrofit
class RetrofitInstance {
companion object {
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
var moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
fun getRetrofitInstance(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.build()
}
}
}
and finally the MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val retService = RetrofitInstance.getRetrofitInstance().create(AlbumsService::class.java)
val responseLiveData: LiveData<Response<Albums>> = liveData {
val response:Response<Albums> = retService.getAlbums()
emit(response)
}
responseLiveData.observe(this, {
val albumsList = it.body()?.listIterator()
if (albumsList != null) {
while (albumsList.hasNext()) {
albumsList.forEach {
Log.d(TAG, "album data by title: ${it.title}")
}
}
}
})
}
}
and while I searching for the cause and solution of this error, I see this question and it look the Moshi cannot recognize the data class Albums and I don't know why, but after I changed the structure of service instead using Albums class to List it's work fine
I replaced the service like this
#GET("albums")
suspend fun getAlbums(): Response<List<AlbumsItem>>
and call it like this
val responseLiveData: LiveData<Response<List<AlbumsItem>>> = liveData {
val response:Response<List<AlbumsItem>> = retService.getAlbums()
emit(response)
}
PS: I tried this annotation #JsonClass(generateAdapter = true) and with value false above the Albums class but it doesn't work, also I tried using GsonConverterFactory with Albums class it's recognize it without problem, so How can I make the moshi recognize it?
Maybe for someone it will works.
In my case I changed
This:
return Retrofit.Builder().addConverterFactory(MoshiConverterFactory.create(MyMoshiObject).asLenient())
.baseUrl(Constants.BASE_URL).
.client(okHttpClient)
.build().
To this:
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create().asLenient())
.baseUrl(Constants.BASE_URL).
.client(okHttpClient)
.build()
Just removed Moshi object in create()
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.
I'm trying to build custom deserializers for the responses I get from OMDb API.
Here's the data class for Movie:
data class Movie(
val title: String?,
val year: String?,
val imdbID: String?,
val type: String?,
val poster: String?,
val mpaRating: String?,
val runtime: String?,
val genres: String?,
val director: String?,
val writers: List<String>?,
val actors: List<String>?,
val plot: String?,
val awards: String?,
val boxOfficeEarnings: String?,
val ratings: List<Rating>,
val response: Boolean?
)
And for Rating:
data class Rating(
#SerializedName("Source")
val source: String,
#SerializedName("Value")
val value: String
)
This is the custom JsonDeserializer so far:
class MovieDeserializer : JsonDeserializer<Movie>
{
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): Movie
{
val movieJsonObject = json?.asJsonObject
return Movie(
movieJsonObject?.get("Title")?.asString,
movieJsonObject?.get("Year")?.asString,
movieJsonObject?.get("imdbID")?.asString,
movieJsonObject?.get("Type")?.asString,
movieJsonObject?.get("Poster")?.asString,
movieJsonObject?.get("Rated")?.asString,
movieJsonObject?.get("Runtime")?.asString,
movieJsonObject?.get("Genre")?.asString,
movieJsonObject?.get("Director")?.asString,
separateStringByComma(movieJsonObject?.get("Writer")?.asString),
separateStringByComma(movieJsonObject?.get("Actors")?.asString),
movieJsonObject?.get("Plot")?.asString,
movieJsonObject?.get("Awards")?.asString,
movieJsonObject?.get("BoxOffice")?.asString,
// this is where I need help,
movieJsonObject?.get("Response")?.asBoolean
)
}
fun separateStringByComma(stringToSeparate: String?): List<String>?
{
return stringToSeparate?.split(", ")
}
}
How can I convert that JsonElement directly to List<Rating> without some json string manipulation?
By the way, I'm using Retrofit with Gson:
val gsonMovieDeserializer = GsonBuilder()
.registerTypeAdapter(Movie::class.java, MovieDeserializer())
.create()
val retrofit = Retrofit.Builder()
.baseUrl("https://www.omdbapi.com/")
.addConverterFactory(GsonConverterFactory.create(gsonMovieDeserializer))
.build()
val omdbApi = retrofit.create(OmdbApi::class.java)
val movie = omdbApi.getMovie(movieImdbId.value.toString())
First of all, I'd like to point the usage of nullables there: instead of checking wheter movieJsonObject is null or not for every call inside deserialize(), you should change the function parameters not to be null and then check only once, right at the beggining, if json is a JsonObject, just skipping everything if it's not. That way, we have a solid base to extract the data. Also, for the Movie data class, check the API documentation for which fields are optional and only set those to nulalble (I'm pretty sure at least the title and ID there are always present, so it's way more useful to have them as non-nullable).
Now, for the question itself, you should probably be able to deserialize that list using context.deserialize<List<Rating>>(movieJsonObject.get("Ratings"), List::class.java), which, in Kotlin, will return a type-safe List<Rating> (but, again, make sure that's not an optional field in the API and, if it is, make it nullable).
Hi I am using gson library to map values of response to model. I am doing it like this but it is not mapping values of response to model. I am getting list of 50 models but values in it is zero.
#Provides
#Singleton
fun provideRestApiHelper(
okHttpClient: OkHttpClient,
gson: Gson,
rxJava2CallAdapterFactory: RxJava2CallAdapterFactory): RestApi {
val builder = Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addCallAdapterFactory(rxJava2CallAdapterFactory)
.addConverterFactory(GsonConverterFactory.create(gson))
val retrofit = builder.client(okHttpClient).build()
return retrofit.create(RestApi::class.java)
}
RestApi.kt
interface RestApi {
#GET(ApiEndPoint.ENDPOINT_GITHUB_JOBS)
fun getJobsApiCall(): Observable<List<JobsResponse>>
}
ApiHelperImpl.kt
class ApiHelperImpl #Inject constructor(private val restApi: RestApi) : ApiHelper {
override fun getJobsApiCall(): Observable<List<JobsResponse>> {
return restApi.getJobsApiCall()
}
}
JobsResponse.kt
data class JobsResponse(
#field:SerializedName("company_logo")
val companyLogo: String?,
#field:SerializedName("how_to_apply")
val howToApply: String?,
#field:SerializedName("created_at")
val createdAt: String?,
#field:SerializedName("description")
val description: String?,
#field:SerializedName("location")
val location: String?,
#field:SerializedName("company")
val company: String?,
#field:SerializedName("company_url")
val companyUrl: String?,
#field:SerializedName("id")
val id: String?,
#field:SerializedName("title")
val title: String?,
#field:SerializedName("type")
val type: String?,
#field:SerializedName("url")
val url: String?
) : BaseResponse()
I am calling this API https://jobs.github.com/positions.json. Does anyone know what could be the issue ?
That's because you rely on auto-converted java code
remove #field:SerializedName changed it to #SerializedName
don't put them in to primary constructor
define them like this:
data class JobsResponse(){
#SerializedName("company_logo")
val companyLogo: String? = null
....
}