Retrofit2 & JSON response array with multiple possible values - android

I haven't written anything here for a long time, but now I really need advice )
I'm using Retrofit2 as api client. Server API has one endpoint, for example /api/stats which receive JSON body request and return JSON response as:
data class StatsResult<T>(
#SerializedName("code") val code: Int,
#SerializedName("message") val msg: String?,
#SerializedName("request_id") val requestId: String?,
#SerializedName("data") val data: T?
)
If some error, data is null.
Otherwise data is an array that can contain different type of data depending on the type of request.
For example:
Request1:
{
"type": "type1",
"params": {
}
}
Response:
{
"code": 0,
"request_id": "...",
"data": [
{
"key1": "value1",
"key2": "value2"
},
{
"key1": "value3",
"key2": "value4"
}
]
}
Request2:
{
"type": "type2",
"params": {
}
}
Response:
{
"code": 0,
"request_id": "...",
"data": [
{
"key3": "value1",
"key4": "value2"
},
{
"key3": "value3",
"key4": "value4"
}
]
}
Here is my implementation in short:
interface StatsApi {
#POST("/api/stats")
suspend fun getStats(#Body request: StatsRequest): ApiResponse<StatsData>
}
sealed class ApiResponse<out T: Any> {
data class Success<T: Any>(val body: T): ApiResponse<T>()
object Unauthorized : ApiResponse<Nothing>()
object Forbidden: ApiResponse<Nothing>()
object NetworkError: ApiResponse<Nothing>()
data class Error(val msg: String? = null): ApiResponse<Nothing>()
data class Exception(val t: Throwable): ApiResponse<Nothing>()
}
typealias StatsData = StatsResult<List<BaseStatsDto>>
open class BaseStatsDto()
class Type1StatsDto: BaseStatsDto() {
#SerializedName("key1") var key1: String? = null
#SerializedName("key2") var key2: String? = null
}
class Type2StatsDto: BaseStatsDto() {
#SerializedName("key3") var key3: String? = null
#SerializedName("key4") var key4: String? = null
}
So I tried to workaround this with open/abstract class BaseStatsDto and than cast it to final class. But this solution didn't work.
For response handling I'm using CallAdapter.Factory() with custom Call<>:
open class ApiResponseCall<T : Any>(
private val delegate: Call<T>
) : Call<ApiResponse<T>> {
override fun enqueue(callback: Callback<ApiResponse<T>>) {
return delegate.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
val rsp = when (t) {
is IOException -> ApiResponse.NetworkError
else -> ApiResponse.Exception(t)
}
callback.onResponse(this#ApiResponseCall, Response.success(rsp))
}
override fun onResponse(call: Call<T>, response: Response<T>) {
val rsp: ApiResponse<T>
rsp = if (response.isSuccessful) {
val body = response.body()
ApiResponse.Success(body as T)
} else {
val code = response.code()
val error = response.errorBody()
when (code) {
401 -> ApiResponse.Unauthorized
403 -> ApiResponse.Forbidden
in 400..499 -> ApiResponse.Error("Client error")
in 500..599 -> ApiResponse.Error("Server error")
else -> ApiResponse.Exception(Exception("Unknown error"))
}
}
callback.onResponse(this#ApiResponseCall, Response.success(rsp))
}
})
}
...
}
I see another solution - to have separate interface functions with separate response types. And it working fine:
#POST("/api/stats")
suspend fun getType1Stats(#Body request: StatsRequest): ApiResponse<StatsResult<List<Type1StatsDto>>>
#POST("/api/stats")
suspend fun getType2Stats(#Body request: StatsRequest): ApiResponse<StatsResult<List<Type2StatsDto>>>
But if statistic data types count increases it will be very uncomfortable to maintain.
I would like to have one statistic api endpoint.

Use http://www.jsonschema2pojo.org/ Set SourceType: JSON and Annonation style: Gson
Data for request 1
class Data1 {
#SerializedName("key1")
#Expose
var key1: String? = null
#SerializedName("key2")
#Expose
var key2: String? = null
}
Response of request1
class Response1 {
#SerializedName("code")
#Expose
var code: Int? = null
#SerializedName("request_id")
#Expose
var requestId: String? = null
#SerializedName("data")
#Expose
var data: List<Data1>? = null
}
Data for request 2
class Data2 {
#SerializedName("key3")
#Expose
var key3: String? = null
#SerializedName("key4")
#Expose
var key4: String? = null
}
Response of request 2
class Response1 {
#SerializedName("code")
#Expose
var code: Int? = null
#SerializedName("request_id")
#Expose
var requestId: String? = null
#SerializedName("data")
#Expose
var data: List<Data2>? = null
}
with
#POST("/api/stats")
suspend fun getStats1(#Body request: StatsRequest1): ApiResponse<Response1>
#POST("/api/stats")
suspend fun getStats2(#Body request: StatsRequest2): ApiResponse<Response2>

Related

Retrofit handle different responses kotlin

I'm trying to handle 2 different responses with retrofit, but I can't get what I needed.
What I needed is
Success : "status": true, "message": "...", "user": {...}, "data": {...}.
Error : {"status":false, "message":"Not authenticated"}.
But what I get is my default setting from OkHttpClient interceptor.
For the retrofit call :
interface ProfileServices {
#Headers("#: Auth")
#GET("profile")
suspend fun getProfile(
): ResponseSuccess<ProfileModel>
}
This is my code for success response :
class ResponseSuccess<out T>(
#Json(name = "status") val status: Boolean,
#Json(name = "message") val message: String,
#Json(name = "user") val user: T,
#Json(name = "data") val data: T
)
And for the error response :
class ErrorResponse(
#Json(name = "status") val status: Boolean,
#Json(name = "message") val message: String
)
My Example ViewModel :
var profileResponse = MutableLiveData<ProfileModel>()
fun getProfile() {
viewModelScope.launch {
profileState.sendAction(UiState.Loading)
try {
val response = services.getProfile()
if (response.status) {
profileState.sendAction(UiState.Success)
profileResponse.postValue(response.user!!)
} else {
profileState.sendAction(UiState.Error(response.message))
}
} catch (error: Exception) {
profileState.sendAction(UiState.Error(error.errorMesssage))
}
}
}

Handle dynamic response sometimes object / array on same key on android kotlin

I have a response from this api, and there is different response on
...
"value": [
{
"#unit": "C",
"#text": "28"
}
]
sometimes
"value":
{
"#unit": "C",
"#text": "28"
}
I have try create this json adapter & model class from the answer
object WeatherResponse {
open class CuacaResponse{
#SerializedName("Success")
val success : Boolean = false
val row : RowBean? = null
}
data class RowBean(
val data : DataBean? = null
)
data class DataBean (
val forecast : ForecastBean? = null
)
data class ForecastBean(
val area : List<Area>? = null
)
data class Area(
#SerializedName("#id")
val id :String?="",
#SerializedName("#description")
val nama :String?="",
val parameter : List<DataMain>?=null
)
data class DataMain(
#SerializedName("#description")
val namaData :String?="",
#SerializedName("#id")
val id :String?="",
#SerializedName("timerange")
val timeRange : List<TimeRangeItem>
)
data class TimeRangeItem(
// sample data : 202107241800 => 2021-07-24-18:00
#SerializedName("#datetime")
val datetime : String,
#JsonAdapter(ValueClassTypeAdapter::class)
val value : ArrayList<ValueData>? = null,
)
data class ValueData(
#SerializedName("#unit")
val unit :String?="",
#SerializedName("#text")
val value :String?="",
)
class ValueClassTypeAdapter :
JsonDeserializer<ArrayList<ValueData?>?> {
override fun deserialize(
json: JsonElement,
typeOfT: Type?,
ctx: JsonDeserializationContext
): ArrayList<ValueData?> {
return getJSONArray(json, ValueData::class.java, ctx)
}
private fun <T> getJSONArray(json: JsonElement, type: Type, ctx:
JsonDeserializationContext): ArrayList<T> {
val list = ArrayList<T>()
if (json.isJsonArray) {
for (e in json.asJsonArray) {
list.add(ctx.deserialize<Any>(e, type) as T)
}
} else if (json.isJsonObject) {
list.add(ctx.deserialize<Any>(json, type) as T)
} else {
throw RuntimeException("Unexpected JSON type: " + json.javaClass)
}
return list
}
}
}
my retrofit service :
interface WeatherService {
#GET("/api/cuaca/DigitalForecast-{province}.xml?format=json")
suspend fun getWeather(
#Path("province") provinceName: String? = "JawaTengah"
) : WeatherResponse.CuacaResponse?
companion object {
private const val URL = "https://cuaca.umkt.ac.id"
fun client(context: Context): WeatherService {
val httpClient = OkHttpClient.Builder()
httpClient.apply {
addNetworkInterceptor(
ChuckerInterceptor(
context = context,
alwaysReadResponseBody = true
)
)
addInterceptor { chain ->
val req = chain.request()
.newBuilder()
.build()
return#addInterceptor chain.proceed(req)
}
cache(null)
}
val gsonConverterFactory = GsonConverterFactory.create()
return Retrofit.Builder()
.baseUrl(URL)
.client(httpClient.build())
.addConverterFactory(gsonConverterFactory)
.build()
.create(WeatherService::class.java)
}
}
}
But the result that i got, from log request :
...
{
"#datetime": "202108070000",
"value": {
"size": 4
}
},
{
"#datetime": "202108070600",
"value": {
"size": 4
}
},
{
"#datetime": "202108071200",
"value": {
"size": 4
}
}
...
the value return size that IDK from where, it should return array of unit & text from the api.
Please anyone help me from this stuck, thanks in advance!
Finaly, i have solved this by change JsonDeserializer to TypeAdapterFactory as mentioned on this answer

Why is my response body null, status 200?

I am trying to get response body from this url:http:"//192.168.0.220:8000/records/?account_id=2"
In android studio i get status 200 but body is always null. Can anybody please tell me what I am doing wrong?
Response in postman looks like this:
{
"result": [
{
"id": 1,
"account_id": 2,
"title": "ez",
"datetime": "2021-03-21T00:00:00",
"description": "ez2",
"image": null,
"recording": null
},
{
"id": 2,
"account_id": 2,
"title": "ez",
"datetime": "2021-03-21T00:00:00",
"description": "ez2",
"image": null,
"recording": null
},
....
Response in android studio:
I/System.out: Response{protocol=http/1.1, code=200, message=OK, url=http://192.168.0.220:8000/records/?account_id=2}
Item(id=null, account_id=null, title=null, datetime=null, image=null, recording=null)
Interface:
interface Gett {
#GET("?account_id=2")
fun getRecord(): Call<Record.Item>
}
Class:
class Record {
data class Item(
val id: String,
val account_id: String,
val title: String,
val datetime: String,
val image: String,
val recording: String
)
}
MainActivity:
val retrofit = Retrofit.Builder()
.baseUrl("http://192.168.0.220:8000/records/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val service = retrofit.create(Gett::class.java)
val call = service.getRecord()
call.enqueue(object : retrofit2.Callback<Record.Item> {
override fun onResponse(call: Call<Record.Item>, response: Response<Record.Item>) {
if (response.code() == 200) {
println(response)
println(response.body()!!)
}
}
override fun onFailure(call: Call<Record.Item>, t: Throwable) {
println("fail")
}
})
The issue might be this
interface Gett {
#GET("?account_id=2")
fun getRecord(): Call<Record.Item> <----
}
So, change your model
data class Record (
val result: List<Item>
)
data class Item(
val id: String,
val account_id: String,
val title: String,
val datetime: String,
val image: String,
val recording: String
)
As I can see your JSON has an array of your Item so change it to.
interface Gett {
#GET("?account_id=2")
fun getRecord(): Call<Record>
}
Thanks, #Kunn for pointing out JSONObject.

Could not map the json data to pojo class using retrofit

As I viewed the success API response, it seems it is sending the data but the data could not load to its corresponding Pojo class, where the size of ArrayList seems null. The POJO class structure seems fine from my side but could not figure out what the problem is. Here I've provided my log screenshot which returned the API data and the Log I kept to view the ArrayList size:
API Response:
{
"achievementList": [
{
"id": "somerandomuuid2",
"name": "Foodie",
"url": "https://toppng.com/uploads/preview/achievement-icon-icon-11553495882s4jdqrtwe2.png",
"rewardPoint": 50,
"description": "Order 500 Food items.",
"earnPoint": 500,
"userPoint": 450,
"achieved": false
},
{
"id": "somerandomuuid3",
"name": "Explorer",
"url": "https://toppng.com/uploads/preview/achievement-icon-icon-11553495882s4jdqrtwe2.png",
"rewardPoint": 50,
"description": "Book more than 100 tickets.",
"earnPoint": 100,
"userPoint": 0,
"achieved": false
}
],
"totalRewardPoints": 0
}
Achievements (POJO Class)
data class Achievements(
#Json(name = "achievementList")
var achievementsList: ArrayList<AchievementsList>
)
data class AchievementsList(
#Json(name = "id")
var id: String?,
#Json(name = "name")
var name: String?,
#Json(name = "url")
var url: String?,
#Json(name = "rewardPoint")
var rewardPoint: Int?,
#Json(name = "description")
var description: String?,
#Json(name = "earnPoint")
var earnPoint: Int?,
#Json(name = "userPoint")
var userPoint: Int?,
#Json(name = "achieved")
var achieved: Boolean?
)
APIService
fun getUserAchievements(
context: AppCompatActivity,
userId: String,
listener: OnAchievementsListener
) {
APIClient.normalRequest.getUserAchievements(userId)
.enqueue(object : Callback<Achievements> {
override fun onFailure(call: Call<Achievements>, t: Throwable) {
listener.onFailure(ResponseCodes.badRequest, t.localizedMessage!!)
}
override fun onResponse(
call: Call<Achievements>,
response: Response<Achievements>
) {
when (response.code()) {
ResponseCodes.success -> {
Log.i("ProfileActivity: ", "Response: ${response.body()}")
response.body()?.let { listener.onSuccess(it) }
}
else -> {
when (response.code()) {
listener.onFailure(response.code(), "Something went wrong")
}
}
}
}
})
}
Activity
private fun getAchievementsList() {
appPreferences?.getString(AppPreferences.UUID)?.let {
getUserAchievements(this, it, object : OnAchievementsListener {
override fun onFailure(code: Int, description: String) {
Utils.showToast(this#ProfileActivity, "$code, $description")
}
override fun onSuccess(achievements: Achievements?) {
Log.i(TAG, "Size: ${achievements?.achievementsList?.size}")
}
})
}
}
Try this:
#SerializedName("achievementList")
instead of
#Json(name = "achievementList")
in POJO class all field

Unable to parse JSON using Retrofit in Android

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

Categories

Resources