Using default JsonDeserializer inside custom JsonDeserializer in Kotlin - android

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

Related

something wrong with my API call, using Retrofit with moshi converter

I am learning Android development and I am trying to build a simple news app.
but I have been stuck for 3 days not able to fetch the json response from the server.
I think something wrong with my retrofit call but I can't figure what it is. I check the logcat and there is no response printed !
I appreciate any help. (I changed my API key because it is personal)
private const val BASE_URL = "https://newsapi.org"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
interface CallNewsApi {
#GET("/v2/top-headlines")
suspend fun getHeadlines(
#Query("apiKey") key: String,
#Query("category") category: String,
#Query("country") country: String
): Response<NewsApiResponse>
}
object NewsApi {
val retrofitService: CallNewsApi by lazy {
retrofit.create(CallNewsApi::class.java)
}
}
data class NewsApiResponse(
var status: String,
var totalResults: Int,
var articles: List<NewsHeadlines>
)
data class NewsHeadlines(
var source: Source,
var author: String,
var title: String,
var description: String,
var url: String,
var urlToImage: String,
var publishedAt: String,
var content: String
)
data class Source(
var id: String,
var name: String
)
class NewsViewModel : ViewModel() {
private val _newsList = MutableLiveData<List<NewsHeadlines>>()
val status: LiveData<List<NewsHeadlines>> = _newsList
init {
getNewsLines()
}
private fun getNewsLines() {
viewModelScope.launch {
try {
Log.d("ViewModel"," before retrofit")
val call = NewsApi.retrofitService.getHeadlines(
"myApiKey",
"sports",
"il"
)
Log.d("ViewModel", call.body().toString())
} catch (e: java.lang.Exception) {
_newsList.value = listOf()
}
}
}
}
Looking at the sample responses in the API documentation, it seems that a lot of the fields in the json response can be null like source.id, author, content, description and urlToImage. For these fields you should use nullable properties i.e. String?.
Since the documentation doesn't explicitly mention which fields can be null and which can't be, it's upto you whether you want to mark all of them as nullable or not. But you can skip those which you think will always be present. For example, I think that status, totalResults, publishedAt, title, source.name should always be present in the news response, so you can choose to keep these as non-nullable.

Is this expression to get a nested array possible with Retrofit?

In a previous question some users commented as a response to access a nested array that this is possible:
fun getMovies(): Single<Response<List<Result>>>
But every time that I try to implement it it gives me the Error message:
No type arguments expected for class Response
This is my Model Class:
data class Response(
val page: Int?,
val total_results: Int?,
val total_pages: Int?,
val results: List<Result>
) {
//#Entity
data class Result(
val popularity: Double?,
val vote_count: Int?,
val video: Boolean?,
val poster_path: String?,
val id: Int?,
val adult: Boolean?,
val backdrop_path: String?,
val original_language: String?,
val original_title: String?,
val genre_ids: List<Int>?,
val title: String?,
val vote_average: Double?,
val overview: String?,
val release_date: String?
)
}
My Api Interface
package com.example.moviesapplication.model
import io.reactivex.Single
import retrofit2.http.GET
interface MoviesApi {
#GET("/3/discover/movie?api_key=${apiKey}&language=en-US&sort_by=popularity.desc&include_adult=false&include_video=false&page=1")
fun getMovies(): Single<List<Response.Result>>
}
So if anyone know if the expression stated is possible or something similar, please let me know.
Your Response class doesn't have any generic parameters, and that's what the error is pointing out.
You can return Single<Response> and the array will always be a List of Result, it doesn't have any dynamic types.
I think you import wrong package, maybe you import Response of Okhttp instead of your Response data class since Response of OkHttp doesn't want type argument.
At the same time your Response is a bit strange, Your Response class does not want type argument. Your response should be something like
fun getMovies(): Single<Response>
instead of
fun getMovies(): Single<Response<List<Result>>>
I think it become like this because you confuse between Response of Retrofit that need type argument [Response<T>] and your Response data class that does not need type argument.
Recommend
I thinks it will be better if your Response class is some think like
data class Response<T>(
val page: Int?,
val total_results: Int?,
val total_pages: Int?,
val results: List<T> // or val results: T
)
Because if you have lots pf responses that have Base like Response class but, its result have different model or list (something likeList<ResultMovie> or List<...>). You only need to pass T to your Base model like Response<Result>. You not need to repeat to declare Response every time you have Base model like this, and it also easier to manage or fixes later.

Why am I getting null values when using gson to parse a json object in android studio?

I am reading in the following JSON and using GSON to convert it to an object which I can then use to access the properties of said object to help display the images in my app.
However one of the field I want to use, imageURL, is returning null values. Looking at the json (link below) we can clearly see that it is not null.
https://api.letsbuildthatapp.com/youtube/home_feed
I have used the debugger to demonstrate the null value I am getting for each imageURL:
Debugger output
So the object is giving me null values for the imageURL but the link is not. What is going on?
Here is the function I wrote to fetch and parse the JSON object:
private fun fetchJson() {
println("Attempting to fetch JSON")
val url = "https://api.letsbuildthatapp.com/youtube/home_feed"
val request = Request.Builder().url(url).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
val gson = GsonBuilder().create()
val homeFeed = gson.fromJson(body, HomeFeed::class.java)
runOnUiThread {
recyclerView_main.adapter = MainAdapter(homeFeed)
}
}
override fun onFailure(call: Call, e: IOException) {
println("failed to execute request")
}
}
)
}
My HomeFeed class is like this:
class HomeFeed(val videos: List<Video>)
class Video(val id: Int, val name: String, val link: String, val imageURL: String, numberOfViews: Int,
val channel: Channel)
class Channel(val name: String, val profileImageUrl: String)
I believe this should be detailed enough but if anymore info is needed please let me know.
Thank you.
Try with this object, you should use the same name for the vals, "imageUrl" instead of "imageURL":
data class HomeFeed(
val user: User,
val videos: List<Video>
)
data class User(
val id: Int,
val name: String,
val username: String
)
data class Video(
val channel: Channel,
val id: Int,
val imageUrl: String,
val link: String,
val name: String,
val numberOfViews: Int
)
data class Channel(
val name: String,
val numberOfSubscribers: Int,
val profileImageUrl: String
)

Retrofit and interface to model inheritance in data classes of Kotlin

I have a get request that returns certain features where 11 properties of the objects are common. Since data classes in Kotlin can't inherit from other classes i decided to define the common properties with the following interface.
interface AccountFeature {
val feature: String?
val status: String?
val id: String?
val urlLogo: String?
val minAppVersion: String?
val target: String?
val title: String?
val backgroundColor: String?
val bodyTextColor: String?
val bodyText: String?
val titleTextColor: String?
}
One of the data classes look like this.
data class AccountFeatureHelp(val privacyStatement: String? = null,
val supportFAQ: String? = null,
val termsOfService: String? = null,
val supportHotline: String? = null,
val supportEmail: String? = null,
override val feature: String?,
override val status: String?,
override val id: String?,
override val urlLogo: String?,
override val minAppVersion: String?,
override val target: String?,
override val title: String?,
override val backgroundColor: String?,
override val bodyTextColor: String?,
override val bodyText: String?,
override val titleTextColor: String?): AccountFeature
So far everthing is fine.
In the Interface definition of my Request, i expect an observable array of AccountFeature. My goal is depending on the feature value, mapping the hashmap to corresponding Feature object.
interface AccountFeaturesAPIService {
#GET("accounts/{id}/features")
fun getAccountFeature(#Path("id") id: String): Observable<Array<AccountFeature>>
}
I get the following runtime exception.
java.lang.RuntimeException: Unable to invoke no-args constructor for interface com.thinxnet.native_tanktaler_android.core.model.account.feature.AccountFeature. Registering an InstanceCreator with Gson for this type may fix this problem.
How would i overcome this in an elegant way apart from changing Observable> to Observable>

GSON is not mapping values with data class in kotlin

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
....
}

Categories

Resources