Issue
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)
}
Solution
Everything is explained here (1min read) Api is supposed to return "{}" for null, If you can't change API add this converter to Retrofit
Answered By - Vojin Purić