Search code examples
androidkotlinretrofit2json-deserializationandroid-json

Android Retrofit2: Serialize null without GSON or JSONObject


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