Unable to parse JSON using Retrofit in Android-ThrowExceptions

Exception or error:

I am successfully able to hit the API and get the json result. I can see the success result in the logs by printing Retrofit response body. and also using Stetho as the network interceptor.

However, I am not able to understand why is the api response still “null” in the onResponse() method in the repository. I believe, I am not passing the correct model maybe for the JSON to be parsed properly ? Can anybody help me to find out what’s the issue here?

Following is the json:

{
  "photos": {
    "page": 1,
    "pages": 2864,
    "perpage": 100,
    "total": "286373",
    "photo": [
      {
        "id": "49570734898",
        "owner": "165034061@N07",
        "secret": "f3cb2c2590",
        "server": "65535",
        "farm": 66,
        "title": "Hello",
        "ispublic": 1,
        "isfriend": 0,
        "isfamily": 0
      }
    ],
    "photo": [
      {
        "id": "12344",
        "owner": "23444@N07",
        "secret": "f233edd",
        "server": "65535",
        "farm": 66,
        "title": "Hey",
        "ispublic": 1,
        "isfriend": 0,
        "isfamily": 0
      }
    ]
  },
  "stat": "ok"
}

My Pojo Class :

data class Photos(
    @SerializedName("page")
    val page: Int,
    @SerializedName("pages")
    val pages: Int,
    @SerializedName("perpage")
    val perpage: Int,
    @SerializedName("photo")
    val photos: List<Photo>,
    @SerializedName("total")
    val total: String
)

data class Photo(
    @SerializedName("farm")
    val farm: Int,
    @SerializedName("id")
    val id: String,
    @SerializedName("isfamily")
    val isFamily: Int,
    @SerializedName("isfriend")
    val isFriend: Int,
    @SerializedName("ispublic")
    val isPublic: Int,
    @SerializedName("owner")
    val owner: String,
    @SerializedName("secret")
    val secret: String,
    @SerializedName("server")
    val server: String,
    @SerializedName("title")
    val title: String
)

RetrofitClient:

object ApiClient {

    private val API_BASE_URL = "https://api.flickr.com/"

    private var servicesApiInterface: ServicesApiInterface? = null

    fun build(): ServicesApiInterface? {
        val builder: Retrofit.Builder = Retrofit.Builder()
            .baseUrl(API_BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())

        val httpClient: OkHttpClient.Builder = OkHttpClient.Builder()
        httpClient.addInterceptor(interceptor()).addNetworkInterceptor(StethoInterceptor())

        val retrofit: Retrofit = builder
            .client(httpClient.build()).build()
        servicesApiInterface = retrofit.create(
            ServicesApiInterface::class.java
        )

        return servicesApiInterface as ServicesApiInterface
    }

    private fun interceptor(): HttpLoggingInterceptor {
        val httpLoggingInterceptor = HttpLoggingInterceptor()
        httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        return httpLoggingInterceptor
    }

    interface ServicesApiInterface {

        @GET("/services/rest/?method=flickr.photos.search")
        fun getImageResults(
            @Query("api_key") apiKey: String,
            @Query("text") text: String,
            @Query("format") format: String,
            @Query("nojsoncallback") noJsonCallback: Boolean
        ): Call<PhotoResponse>

    }

}

OperationCallback:

interface OperationCallback<T> {
    fun onSuccess(data:List<T>?)
    fun onError(error:String?)
}

PhotoDataSource:

interface PhotoDataSource {

    fun retrievePhotos(callback: OperationCallback<Photo>, searchText: String)
    fun cancel()
}

PhotoRepository:

class PhotoRepository : PhotoDataSource {
 private var call: Call<PhotoResponse>? = null

    private val API_KEY = "eff9XXXXXXXXXXXXX"
    val FORMAT = "json"

    companion object {
        val TAG = PhotoRepository::class.java.simpleName
    }

    override fun retrievePhotos(callback: OperationCallback<Photo>, searchText: String) {
        call = ApiClient.build()
            ?.getImageResults(
                apiKey = API_KEY,
                text = searchText,
                format = FORMAT,
                noJsonCallback = true
            )
        call?.enqueue(object : Callback<PhotoResponse> {
            override fun onFailure(call: Call<PhotoResponse>, t: Throwable) {
                callback.onError(t.message)
            }

            override fun onResponse(
                call: Call<PhotoResponse>,
                response: Response<PhotoResponse>
            ) {

                response?.body()?.let {
                    Log.d(TAG, "got api response total pics are  :${it.data?.size}")
                    if (response.isSuccessful && (it.isSuccess())) {
                        callback.onSuccess(it.data)
                    } else {
                        callback.onError(it.msg)
                    }
                }
            }
        })
    }

    override fun cancel() {
        call?.let {
            it.cancel()
        }
    }
}

PhotoResponse:

data class PhotoResponse(val status: Int?, val msg: String?, val data: List<Photo>?) {
    fun isSuccess(): Boolean = (status == 200)
}
How to solve:

Try to change your PhotoResponse to match with your json response.

data class PhotoResponse(
    @SerializedName("stat")
    val status: String?, 
    @SerializedName("photos")
    val photos: Photos?
) {
    fun isSuccess(): Boolean = status.equals("ok", true)
}

And then inside onResponse, You can get List<Photo> like below:

override fun onResponse(
    call: Call<PhotoResponse>,
    response: Response<PhotoResponse>
) {

    response?.body()?.let {

        //This should be your list of photos
        it.photos.photos
    }
}

###

The issue is with your data class. You need one extra data class here.
So if you look at your JSON response closely, then you will understand whats going wrong.
Your photos data class should not be the first class. Instead it should be inside one more class lets say PhotoApiResponse.
Your first class will contain both photos and stat.
And then rest can be the same.

Leave a Reply

Your email address will not be published. Required fields are marked *