Deserialize a response with Kotlinx.Serialization and Retrofit - android

I'm using kotlinx.serialization and implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.5.0") with Retrofit2
I'm probably missing something obvious, but I'm just not putting 2 and 2 together on this one.
I have a serializable class that has a list of another serializable class:
#Serializable
data class A(
val resultCount: Int,
val results: List<B>
)
#Serializable
data class B(
val x: Int,
val y: Int,
val z: Int
)
And retrofit setup like this:
fun provideRetrofit(): Retrofit {
val contentType = MediaType.get("application/json")
return Retrofit.Builder()
.baseUrl(BaseUrl)
.addConverterFactory(Json.asConverterFactory(contentType))
.build()
}
I have a service:
#GET("search/")
suspend fun GetStuff(): A
When I make the network call GetStuff expecting to get an instance of A, I get this exception:
kotlinx.serialization.SerializationException: Can't locate argument-less serializer for class A. For generic classes, such as lists, please provide serializer explicitly. So I guess it's expecting me to provide a serializer for the List?
I've read I should be able to use a list serializer like B.serializer().list but I'm not sure where to use that in my context, and when I try to put it anywhere I get Unresolved reference: serializer.

Related

Retrofit wrap response in a generic class internally

I have a sealed class like below,
sealed class ApiResult<T>(
val data: T? = null,
val errors: List<Error>? = null
) {
class Success<T>(data: T?) : ApiResult<T>(data)
class Failure<T>(
errors: List<Error>,
data: T? = null
) : ApiResult<T>(data, errors)
}
And this is my Retrofit API interface,
interface Api {
#POST("login")
suspend fun login(#Body loginRequestDto: LoginRequestDto): ApiResult<LoginResponseDto>
}
What I want to achieve is, internally wrap the LoginResponseDto in BaseResponseDto which has the success, error, and data fields. Then put them in the ApiResult class.
data class BaseResponseDto<T>(
val success: Boolean,
val errors: List<Int>,
val data: T?
)
In this case, LoginResponseDto is my data class. So, Retrofit should internally wrap the LoginResponseDto in BaseResponseDto then I will create the ApiResult response in my custom call adapter. How can I tell Retrofit to internally wrap this? I don't want to include the BaseResponseDto in the API interface every time.
After spending the whole day, I could achieve it. I had to create a custom converter. Here it is,
class ApiConverter private constructor(
private val gson: Gson
) : Converter.Factory() {
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
val baseResponseType = TypeToken.get(BaseResponseDto::class.java).type
val finalType = TypeToken.getParameterized(baseResponseType, type)
return Converter<ResponseBody, Any> { value ->
val baseResponse: BaseResponseDto<*> =
gson.fromJson(value.charStream(), finalType.type)
baseResponse
}
}
companion object {
fun create(gson: Gson) = ApiConverter(gson)
}
}
Now I don't have to specify the BaseResponseDto every time in the API interface for retrofit.
Great question!
If you want to implement your own solution you would need a custom Retrofit CallAdapter.
My recommendation would be Sandwich an Open source Library which does the exact same thing.
If you want to implement your own soultion here's one way
Modeling Retrofit Responses With Sealed Classes and Coroutines

Creating json from generic strings

I'm trying to do something really simple (at least, should be IMO): create a function that receives a string containing some json and turns that into a Gson object. I've created the following function in my class:
class EasyJson(val url: String, private val responseDataClass: Class<*>) {
var text: String
var json: Gson
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass::class.java) as Gson
}
private fun <T> stringToJson(data: String, model: Class<T>): T {
return Gson().fromJson(data, model)
}
And here is the calling class:
class CallingClass() {
val url="https://api"
init {
test()
}
data class ApiResponse(
val count: Number,
val next: Number?,
val previous: Number?
)
private fun test(){
val json = EasyJson(url, ApiResponse::class.java)
}
}
However, when I do this I get the following exception:
java.lang.UnsupportedOperationException: Attempted to deserialize a java.lang.Class. Forgot to register a type adapter?
How can I use a generic data class as a parameter for Gson here?
The issue here is most likely the following line:
stringToJson(text, responseDataClass::class.java)
This will use the class of the class, which is always Class<Class>. You should just call your stringToJson function with responseDataClass (without ::class.java):
stringToJson(text, responseDataClass)
Additionally it looks like you treat the stringToJson result incorrectly. The result is an instance of T (actually T? because Gson.fromJson can return null). However, you are casting it to Gson.
The correct code would probably look like this:
class EasyJson<T>(val url: String, private val responseDataClass: Class<T>) {
var text: String
var json: T?
init {
text = getJsonFromUrl(URL(url)) //another function does this and is working fine
json = stringToJson(text, responseDataClass)
}
private fun <T> stringToJson(data: String, model: Class<T>): T? {
return Gson().fromJson(data, model)
}
}
(Though unless your code contains more logic in EasyJson, it might make more sense to move this JSON fetching and parsing to a function instead of having a dedicated class for this.)

Moshi cannot parse nullable

Hello) Hope you can help me.
Using kotlin (Retrofit2 + moshi) i getting data from "https://api.spacexdata.com/v3/launches" and parsing it.
All is going fine (i getting attributes like: flight_number, mission_name), but some attributes have "null", like "mission_patch" - there are 111 objects. 109 of them have data at "mission_patch", 2 objects dont have it ("mission_patch":null).
My problem: moshi cannot parse correctly attribute which contains null.
if i using:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String)
i getting error: "Failure: Required value "mission_patch" missing at $[1]" - OK i changed data class to next:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val mission_patch: String?)
with this i getting data, but every object have mission_patch=null. This is uncorrect, bc only 2 objects have mission_patch=null, not all.
Help me please. im new at kotlin, what i doing wrong?
My retrofit service:
private const val BASE_URL = "https://api.spacexdata.com/v3/"
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
//.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
interface SpaceXApiService {
#GET("launches")
suspend fun getProperties():List<SpaceXProperty>
}
object SpaceXApi{
val retrofitservice :SpaceXApiService by lazy {
retrofit.create(SpaceXApiService::class.java)
}
}
mission_patch is not in the root object like flight_number etc. It's nested inside links. So your model should match. Try this:
data class SpaceXProperty(
val flight_number: Int,
val mission_name: String,
val links: Links) {
data class Links(val mission_patch: String?)
}

How to pass data class as parameter in kotlin extension

i have a data class like:
data class GetDoctorResponse(
val BrickName: String,
val Class: String,
val DoctorAddress
)
Now i want to get response, so i get it like this:
val myResponse = Gson().fromJson("response in json", Array<GetDoctorResponse>::class.java).toList()
my question is how can i create an extension which takes data and class and return me response like above.
i have tried this:
fun getResponse(data: String, it: Class<T>) : List<it> =
Gson().fromJson(data, Array<it>::class.java).toList()
but T is unresolved here, and i want to get response of any data class i passed.
val response = getResponse(data, SomeClass())
You need to Pass type also.
I didn't test it so let me know it worked or not.
inline fun <reified T : Any> getResponse(data: String) : List<T> =
Gson().fromJson(data, Array<T>::class.java).toList()
Thanks every one who helped me.
what i did is create this extension
fun <T> getResponse(data: String, model: Class<Array<T>>): List<T> =
Gson().fromJson(data, model).toList()
and call it as
val myResponse = getResponse(data, Array<GetDoctorResponse>::class.java)
and its working.

2 Different objects in a Retrofit response

I have a Retrofit request which can reply two different Json responses one response at a time. I want to catch both cases in onResponse. So what i did is create a BaseResponse class
data class ResponseBase(val responseClass: ResponseClass?, val errorClass: ErrorClass?)
and the two inside class are like that.
data class ResponseClass(
val config: Config
)
// Config class
data class Config(
val acceptGuest: Int,
val name: String,
val host: Long
)
And the Error Class is like that
data class ErrorResponse(
val error: Error
)
data class Error(
val id: String,
val message: String
)
So i guess that if the response json comes then i will have the responseClass object otherwise i will have the ErrorObject.
So when i get the following json response from Server which matches the ResponseClass i have an exception.
{"config":{"acceptGuest":0,"name":"server name","host":100}}
Unable to invoke no-args constructor for retrofit2.Call<....ResponseClass>. Registering an InstanceCreator with Gson for this type may fix this problem
What am i missing..?
I use a GSON converter in my retrofit builder.
#Provides
#Singleton
internal fun provideRetrofit(client: OkHttpClient): Retrofit.Builder {
return Retrofit.Builder()
.addCallAdapterFactory(CoroutineCallAdapterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
}

Categories

Resources