Android Retrofit2: Serialize null without GSON or JSONObject - android

The result of API call in my Android app can be a JSON with configuration which is mapped to SupportConfigurationJson class, or just pure null. When I get a JSON, the app works properly, but when I get null, I get this exception:
kotlinx.serialization.json.internal.JsonDecodingException: Expected start of the object '{', but had 'EOF' instead
JSON input: null
I should avoid using GSON in this project. I also found a solution, where API interface will return Response<JSONObject>, and after that my repository should check if this JSONObject is null and map it to SupportConfigurationJson if not. But in the project we always used responses with custom classes so I wonder, is there any other solution to get response with null or custom data class?
GettSupportConfiguration usecase class:
class GetSupportConfiguration #Inject constructor(
private val supportConfigurationRepository: SupportConfigurationRepository
) {
suspend operator fun invoke(): Result<SupportConfiguration?> {
return try {
success(supportConfigurationRepository.getSupportConfiguration())
} catch (e: Exception) {
/*
THIS SOLUTION WORKED, BUT I DON'T THINK IT IS THE BEST WAY TO SOLVE THE PROBLEM
if (e.message?.contains("JSON input: null") == true) {
success(null)
} else {
failure(e)
}
*/
//I WAS USING THROW HERE TO SEE WHY THE APP ISN'T WORKING PROPERLY
//throw(e)
failure(e)
}
}
}
SupportConfigurationJson class:
#Serializable
data class SupportConfigurationJson(
#SerialName("image_url")
val imageUrl: String,
#SerialName("description")
val description: String,
#SerialName("phone_number")
val phoneNumber: String?,
#SerialName("email")
val email: String?
)
SupportConfigurationRepository class:
#Singleton
class SupportConfigurationRepository #Inject constructor(
private val api: SupportConfigurationApi,
private val jsonMapper: SupportConfigurationJsonMapper
) {
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
private suspend fun mapJsonToSupportConfiguration(
supportConfiguration: SupportConfigurationJson?
) = withContext(Dispatchers.Default) {
jsonMapper.mapToSupportSettings(supportConfiguration)
}
}
fun <T> Response<T?>.extractOrThrow(): T? {
val body = body()
return if (isSuccessful) body else throw error()
}
fun <T> Response<T>.error(): Throwable {
val statusCode = HttpStatusCode.from(code())
val errorBody = errorBody()?.string()
val cause = RuntimeException(errorBody ?: "Unknown error.")
return when {
statusCode.isClientError -> ClientError(statusCode, errorBody, cause)
statusCode.isServerError -> ServerError(statusCode, errorBody, cause)
else -> ResponseError(statusCode, errorBody, cause)
}
}
SupportConfigurationApi class:
interface SupportConfigurationApi {
#GET("/mobile_api/v1/support/configuration")
suspend fun getSupportConfiguration(): Response<SupportConfigurationJson?>
}
SupportConfigurationJsonMapper class:
class SupportConfigurationJsonMapper #Inject constructor() {
fun mapToSupportSettings(json: SupportConfigurationJson?): SupportConfiguration? {
return if (json != null) {
SupportConfiguration(
email = json.email,
phoneNumber = json.phoneNumber,
description = json.description,
imageUrl = Uri.parse(json.imageUrl)
)
} else null
}
}
I create Retrofit like this:
#Provides
#AuthorizedRetrofit
fun provideAuthorizedRetrofit(
#AuthorizedClient client: OkHttpClient,
#BaseUrl baseUrl: String,
converterFactory: Converter.Factory
): Retrofit {
return Retrofit.Builder()
.client(client)
.baseUrl(baseUrl)
.addConverterFactory(converterFactory)
.build()
}
#Provides
#ExperimentalSerializationApi
fun provideConverterFactory(json: Json): Converter.Factory {
val mediaType = "application/json".toMediaType()
return json.asConverterFactory(mediaType)
}

Everything is explained here (1min read)
Api is supposed to return "{}" for null, If you can't change API add this converter to Retrofit

You are interacting with your repository directly, i will suggest to use
usecases
to interact with data layer.
Because you are not catching this exception over here, your app is crashing
suspend fun getSupportConfiguration(): SupportConfiguration? =
mapJsonToSupportConfiguration(api.getSupportConfiguration().extractOrThrow())
Usecase usually catch these errors and show useful error msg at the ui.

Related

Custom Error Handler for retrofit failure cases

I'm new to retrofit, and attempting to set a converter that will automatically deserialize error messages for result.errorBody
CustomErrorHandler
class CustomErrorHandler(private val gson: Gson) : Converter<ResponseBody, Error> {
#Throws(IOException::class)
override fun convert(responseBody: ResponseBody): Error {
val error = gson.fromJson(responseBody.charStream(), Error::class.java)
responseBody.close()
throw Exception(error.message)
}
class Factory(private val gson: Gson) : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
val typeToken = TypeToken.get(type)
if (typeToken.rawType != Error::class.java) {
return null
}
return CustomErrorHandler(gson)
}
}
}
Retrofit
val retrofit = Retrofit.Builder().baseUrl("http://192.168.0.1:8080")
.addConverterFactory(CustomErrorHandler.Factory(GsonBuilder().create()))
.addConverterFactory(GsonConverterFactory.create()).build()
Error
data class Error(val code: String, val message: String) {
override fun toString(): String {
return "code: $code, message: $message"
}
}
RoleService
interface RoleService {
#GET("/roles")
#Headers("Accept: application/json")
suspend fun findAll(): Response<List<Role>>
}
Attempt #1
expecting that following throw statement will have result.errorBody as the Error class or please advise on how it's supposed to be done.
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
throw Exception(result.errorBody().toString()) // result.errorBody to be Error class???
}
Attempt #2
A simple approach without using any converter and deserializing errorBody to Error class
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
val e = Gson().fromJson(result.errorBody().toString(), Error::class.java) // fails too
throw Exception(e.message)
}
This inline solution helped, I couldn't get Converter working.
throw Exception(gson.fromJson(result.errorBody()?.string(), Error::class.java)?.message)
complete code
suspend fun findAll(): List<Role>? {
val result = roleService.findAll()
if (result.isSuccessful)
return result.body()
throw Exception(gson.fromJson(result.errorBody()?.string(), Error::class.java)?.message)
}

How to parse response with retrofit2 Kotlin

I'm stuck with parsing the response. In Swift I can make a codable to help parsing the json response. I'm new to Kotlin and I'm working on someone else existing project. I made a data class for string and boolean but I don't know the syntax to parse it. Please help and thank you.
The responseBody json
{
"bearerToken": "########",
"staySignIn": false
}
//Interface
interface PostInterface {
class User(
val email: String,
val password: String
)
#POST("signIn")
fun signIn(#Body user: User): Call<ResponseBody>
//Network handler
fun signIn(email: String, password: String): MutableLiveData<Resource> {
val status: MutableLiveData<Resource> = MutableLiveData()
status.value = Resource.loading(null)
val retrofit = ServiceBuilder.buildService(PostInterface::class.java)
retrofit.signIn(PostInterface.User(email, password)).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
errorMessage(status)
}
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
if (response.code() == 200) {
try {
status.value = //how to parse using the model??
} catch (ex: Exception) {
parseError(400, response.body().toString(), status)
}
} else {
//do something...
}
}
})
return status
}
//Model
data class SignInModel(
#field:SerializedName("bearerToken")
val bearerToken: String? = null,
#field:SerializedName("staySignIn")
val staySignIn: Boolean? = null
)
//Storing value class
class RrefManager constructor(var applicationContext: Context) {
private fun getSharedPrefEditor(): sharedPrefEditor.Editor {
return applicationContext.getSharedPrefEditor(prefStorageName, Context.MODE_PRIVATE).edit()
}
public fun setBearerToken(token: String) {
getSharedPrefEditor().putString("bearerToken", token).apply()
}
public fun setStaySignIn(enabled: Boolean) {
getSharedPrefEditor().putBoolean("staySignIn", enabled).apply()
}
}
//SignIn Button
viewModel.signIn().observe(viewLifecycleOwner, androidx.lifecycle.Observer { v ->
if (v.status == Resource.Status.SUCCESS) {
val model = v.data as SignInModel
pref.setToken(model.token as String) //storing value
pref.setTwoFactorEnabled(model.twoFactorEnabled as Boolean) //storing value
} else if (v.status == Resource.Status.ERROR) {
//do something...
}
})
I think your best option to achieve something like the codable in swift is to use Gson library for parsing api responses.
When you create the retrofit instance you pass the gson converter to the builder like:
val retrofit = Retrofit.Builder()
.baseUrl(BaseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
After you have done that you can make the api return the response you have as the data class, like:
//Interface
interface PostInterface {
#POST("signIn")
fun signIn(#Body user: User): Call<SignInModel>
}
To read the answer from the callback on your class, the response inside the network call is already parsed into your model in the callback. All the retrofit callback should be changed to receive Callback and then you can access directly like status.value = response.body()
For more info you can consult the retrofit library page where it gives all the details and explanations on how to use it correctly.
https://square.github.io/retrofit/

Retrofit 2.6.0: Custom Coroutines CallAdapterFactory

I need to do custom error handling in my api and I wanted to use coroutines with the new version of Retrofit. Since we don't have to use Deferred any longer, our own Jake Wharton wrote this on reddit a month ago
https://github.com/square/retrofit/blob/master/samples/src/main/java/com/example/retrofit/RxJavaObserveOnMainThread.java
But I'm having problems creating the CallAdapterFactory properly.
To be specific, I don't understand: "Delegates to built-in factory and then wraps the value in sealed class"
Is there anyone already using this setup that can help?
Here's the current code
sealed class Results<out T: Any> {
class Success<out T: Any>(val response: T): Results<T>()
class Failure(val message: String, val serverError: ServerError?): Results<Nothing>()
object NetworkError: Results<Nothing>()
}
class ResultsCallAdapterFactory private constructor() : CallAdapter.Factory() {
companion object {
#JvmStatic
fun create() = ResultsCallAdapterFactory()
}
override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
return try {
val enclosedType = returnType as ParameterizedType
val responseType = getParameterUpperBound(0, enclosedType)
val rawResultType = getRawType(responseType)
val delegate: CallAdapter<Any,Any> = retrofit.nextCallAdapter(this,returnType,annotations) as CallAdapter<Any,Any>
if(rawResultType != Results::class.java)
null
else {
object: CallAdapter<Any,Any>{
override fun adapt(call: Call<Any>): Any {
val response = delegate.adapt(call)
//What should happen here?
return response
}
override fun responseType(): Type {
return delegate.responseType()
}
}
}
} catch (e: ClassCastException) {
null
}
}
}
I've created an example of such a factory, you can find it here on GitHub. Also take a look at a similar question: How to create a call adapter for suspending functions in Retrofit?.

Android: Parse inner unstructured Json with Moshi adapters

I have trouble to parse some inner part of JSON (with Moshi) that can vary from a lot and is highly unstructured. Overall it looks like:
response: {
items: [{
type: "typeA",
data: {
"1563050214700-001": {
foo: 123 ....
}
}
}, {
type: "typeB",
data: {
"1563050214700-002": {[
// differs a lot from previous one
{bar: 123 .... }
]}
}
}]
}
And data class structure looks like:
data class Response(
val items: Map<String,List<Item?>>?
) {
data class Item(
val type: String?,
val data: Map<String,List<DataItem?>>?
) {
data class DataItem(
// members highly unstructured
)
}
}
Schema of "DataItem" varies a lot. Looks like Moshi codegen supports adapters that can potentially allow manual parsing of these inner data classes but I'm not able to find the right tutorial or example. Ideally, I want entire Response parsed just as if it were a well-defined JSON.
Here is how I use retrofit/moshi
#Provides
#Singleton
#MyApp
fun provideMyAppRetrofit(context: Context, #MyApp okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.baseUrl(context.getString(R.string.APP_BASE_URL))
.build()
}
#Provides
#Singleton
fun provideMyAppApiService(#MyApp retrofit: Retrofit): MyAppApiService {
return retrofit.create(MyAppApiService::class.java)
}
How do I achieve this? Any sample or reference implementation will be helpful.
welcome to the polymorphic JSON parsing problems word!
We were writing own JSON adapters that are looking like:
internal class CardJsonAdapter(
moshi: Moshi
) : JsonAdapter<Card>() {
private val cardTypeAdapter = moshi.adapter(Card.Type::class.java)
private val amountsWithActionAdapter = moshi.adapter(AmountsWithActionCard::class.java)
private val backgroundImageCardAdapter = moshi.adapter(BackgroundImageCard::class.java)
#Suppress("TooGenericExceptionCaught")
override fun fromJson(reader: JsonReader): Card = try {
#Suppress("UNCHECKED_CAST")
val jsonMap = reader.readJsonValue() as Map<String, Any?>
val type = cardTypeAdapter.fromJsonValue(jsonMap["type"])
createCardWithType(type, jsonMap)
} catch (error: Exception) {
if (BuildConfig.DEBUG) {
Log.w("CardJsonAdapter", "Failed to parse card", error)
}
// Try not to break the app if we get unexpected data: ignore errors and return a placeholder card instead.
UnknownCard
}
override fun toJson(writer: JsonWriter, value: Card?) {
throw NotImplementedError("This adapter cannot write cards to JSON")
}
private fun createCardWithType(type: Type?, jsonMap: Map<String, Any?>) = when (type) {
null -> UnknownCard
Type.AMOUNTS_WITH_ACTION -> amountsWithActionAdapter.fromJsonValue(jsonMap)!!
Type.BACKGROUND_IMAGE_WITH_TITLE_AND_MESSAGE -> backgroundImageCardAdapter.fromJsonValue(jsonMap)!!
}
}
However, it is not anymore required. Moshi supports polymorphic JSON parsing now - https://proandroiddev.com/moshi-polymorphic-adapter-is-d25deebbd7c5
Make a custom adapter
class YourAdapter {
#FromJson
fun fromJson(reader: JsonReader, itemsAdapter: JsonAdapter<ItemsResponse>): List<ItemsResponse>? {
val list = ArrayList<ItemsResponse>()
if (reader.hasNext()) {
val token = reader.peek()
if (token == JsonReader.Token.BEGIN_ARRAY) {
reader.beginArray()
while (reader.hasNext()) {
val itemResponse = itemsAdapter.fromJsonValue(reader.readJsonValue())
itemsResponse?.let {
list.add(itemResponse)
}
}
reader.endArray()
}
}
return list
}
}

Cannot round Gson Doubles to Longs using Retrofit TypeAdapter in Kotlin

Okay my API is giving me a stray 245.00000003 from long ago when it was supposed to only send down whole numbers. I want to make sure I only parse that as a Long with the value of 245, and make sure if the back end guys do that again I just round it down and then go yell at them.
I have tried numerous type adapters, including the #JsonAdapter annotation and the registerHierarchyTypeAdapter function and none of them seem to have any effect whatsoever. What am I doing wrong?
#Module
class CoreModule {
#Provides
internal fun provideGson(): Gson {
return GsonBuilder().registerTypeAdapter(Long::class.java, LongTypeAdapter()).create()
}
}
class LongTypeAdapter : TypeAdapter<Long>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): Long? {
if (reader.peek() === JsonToken.NULL) {
reader.nextNull()
return null
}
val stringValue = reader.nextString()
return try {
stringValue.toLong()
} catch (e: NumberFormatException) {
try { stringValue.toDouble().toLong() }
catch (e: java.lang.NumberFormatException){ 0L }
}
}
#Throws(IOException::class)
override fun write(out: JsonWriter, value: Long?) {
if (value == null) {
out.nullValue()
return
}
out.value(value)
}
}
I assume that code is supposed to examine any Long in my POJO, and when it comes time to matching it to its JSON content, it will return null if it's null, a Long if it can be toLong()'d, a Long if it can be toDouble().toLong()'d, and 0 otherwise.
POJO:
data class OrdersResponse(
#SerializedName("error") val error: Boolean,
#SerializedName("message") val message: String,
#SerializedName("total") val total: Int,
#SerializedName("orders") val orders: List<Orders>
) {
data class Orders(
#SerializedName("id") val id: Long,
#SerializedName("status") val status: String,
#SerializedName("completion_time") val completionTime: Long?,
#SerializedName("total") val total: Long // This is the dodgy value
) {}
}
Ok I figured this out, it is confusing because there are several options.
First of all, the Gson implementation I am providing was not used in the converter factory for Retrofit:
#Provides
#Singleton
fun provideRetrofit(#Named("baseUrlv1") baseUrl: String, okHttpClient: OkHttpClient, gson: Gson): Retrofit {
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(LiveDataCallAdapterFactory<Any>())
.build()
}
Secondly, I am only interested in the Long in this Orders object, so instead of having the type adapter look at every single Long, I am using a JsonDeserializer (more info here):
#Module
class CoreModule {
#Provides
internal fun provideGson(): Gson {
val gson = GsonBuilder()
gson.registerTypeAdapter(OrdersResponse.Orders::class.java, OrdersDeserializer())
return gson.create()
}
}
class OrdersDeserializer : JsonDeserializer<OrdersResponse.Orders> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): OrdersResponse.Orders {
val jsonObject = json.asJsonObject
val total = jsonObject.get("total").asNumber.toLong()
jsonObject.add("total", JsonPrimitive(total))
return Gson().fromJson(jsonObject, OrdersResponse.Orders::class.java)
}
}

Categories

Resources