Gson migration made easy!
Tags: gson, techdebt, migration, datalayer
Published: 2021-12-17 14:04:54

If you are here, you probably know how tedious it could be to deal with old code written with old deprecated tools that, for one reason or another, we never had the time to tackle correctly and try to find a strategy to remove it safely. Sometimes it could even be a nightmare if the code that you have to refactor is not a simple isolated feature.

What if you have a giant network layer full of code that uses a deprecated tool like `Gson` that is serving other features in your application and, in some way, you have to get rid of it without breaking half of your application?
Well, I might not have a solution for the other kinds of migrations, but if you are dealing with this `Gson` nightmare, I have a migration strategy that might save your day. Let's start by understanding why it is such a big problem if you want to remove `Gson` from your codebase.

Nowadays, modern Android applications, are written in Kotlin language, which is the standard de facto in terms of programming language for Android developers. The problem is that `Gson` is written for Java so it lacks proper Kotlin support, especially for nullability.
Since `Gson` is built for Java it fails to understand the difference between nullable and non-nullable types of Kotlin. This means that if you have a DTO with some field declared as non-nullable and, one of these fields, is not returned in an API for any reason, your app will crash because of it.

A standard solution for this problem is to declare all the fields of the DTO as nullable and then map them to a domain object checking first the presence or absence of any field manually.

The result of this approach is a lot of boilerplate code like this:

```kotlin
// Gson Object
data class PersonDTO(
    val id: String?,
    val name: String?,
    val surname: String?,
    val address: String?
)

// Domain Object
data class Person(
    val id: String,
    val name: String,
    val surname: String,
    val address: String?
)

fun PersonDTO.toDomain(): Person {
    if (id == null || name == null || surname == null) {
        // throw an exception in a controlled manner 
        // or use default value if possible
        throw RuntimeException("Some field is null")
    } else {
        return Person(
            id,
            name,
            surname,
            address
        )
    }
}
```

This is an easy example but keep in mind that the more you complicate your network models the harder will become to deal with these kinds of checks.

For example, let's try to complicate a bit the previous code:

```kotlin
// Gson Object
data class PersonDTO(
    val id: String?,
    val name: String?,
    val surname: String?,
    val address: AddressDTO?
)

data class AddressDTO(
    val street: String?,
    val number: Int?,
)
```

In this second example, if we want to map the `PeopleDTO` object to the domain class, we have to check also the fields of the `AddressDTO` class and so on if you add other nested objects in the response.

You can easily understand that this approach can carry on a lot of problems in terms of code complexity and boilerplate code in case of really complex network responses.

Now that we understood the problem let's assume that we want to migrate our network layer to a more Kotlin friendly deserializer like **Kotlinx Serialization**.

The DTO in our first example after the migration will be something like this:

```kotlin
// Kotlinx Serialization Object
@Serializable
data class PersonDTO(
    val id: String,
    val name: String,
    val surname: String,
    val address: String?
)
```

This is an easy change to do, but for complex network responses with many nested objects, this can result in giant PRs with hundreds of files changed only for the migration of a single call.
And, trust me, once you start with the first object of the call these things will never end.
It's like opening Pandora's box.
And the more files are changed the higher is the risk of introducing some bug.

Also, we didn’t mention that technically you should migrate every network call inside your Retrofit interface to switch to a new deserializer.
Luckily, also for this problem, there are some strategies that you can adopt to migrate the responses call-by-call instead of migrating everything in once, but this is another problem (let me know in the comments if you want to read another article from me about this topic).

Well, as I said previously, this is a nightmare.

![gif](https://media.giphy.com/media/McmLoTZCMUkoZGPXU6/giphy.gif?cid=ecf05e47vuw23ppyjg5aaw58n3v0u70r9bfdrre4eu2j3q62&rid=giphy.gif&ct=g)

There is a simple and sustainable strategy that we can apply to perform the migration.

If we compare the `GSON` version of the DTOs with the `Kotlinx Serialization` one, we might see some important differences: we have a lot of dirty DTOs classes with a lot of nullable fields that are not ready to be used with `Kotlinx Serialization` and a lot of checks that we are performing on these classes to see if what we received from the network is well-formed or not.

What we can do here is to migrate each DTO data class separately with the help of a custom **JsonDeserializer** and, using some extensions utils, performs all the checks that we were doing on the dirty DTOs class inside the deserializer.

Looks too complicated? 

![gif](https://media.giphy.com/media/4JVTF9zR9BicshFAb7/giphy.gif)

Don't worry it will be easier than expected when we will see this approach in action so let's check the code!

Here we have the two classes that we want to migrate to `Kotlinx Serialization`:

```kotlin
// Gson Object
data class PersonDTO(
    val id: String?,
    val name: String?,
    val surname: String?,
    val address: AddressDTO?
)

data class AddressDTO(
    val street: String?,
    val number: Int?,
)
```

The first step should be to update our two classes marking the fields as nullable only when there is an optional field and not everywhere.
So after this first step, our classes should look like this:

```kotlin
// Gson Object
data class PersonDTO(
    val id: String,
    val name: String,
    val surname: String,
    val address: AddressDTO?
)

data class AddressDTO(
    val street: String,
    val number: Int,
)
```

Next, we have to create a custom deserializer for each DTO data class that we want to migrate:

```kotlin
class PersonDeserializer : JsonDeserializer<PersonDTO> {

    override fun deserialize(
        json: JsonElement,
        typeOfT: Type,
        context: JsonDeserializationContext
    ): PersonDTO = with(json.asJsonObject) {
        PersonDTO(
            id = getNotNull("id"),
            name = getNotNull("name"),
            surname = getNotNull("surname"),
            address = deserializeAddressDTO(context)
        )
    }

    private fun JsonObject.deserializeAddressDTO(context: JsonDeserializationContext): AddressDTO? =
        context.deserialize(getOrNull<JsonObject>("address"), getType<AddressDTO>())
}

class AddressDeserializer : JsonDeserializer<AddressDTO> {

    override fun deserialize(
        json: JsonElement,
        typeOfT: Type,
        context: JsonDeserializationContext
    ): AddressDTO = with(json.asJsonObject) {
        AddressDTO(
            street = getNotNull("street"),
            number = getNotNull("number")
        )
    }
}
```

These two deserializers are responsible for converting the received JSON to the desired DTO class checking that all the required fields are present.
But as you can see the code is not that complicated, right?
Well, this is because here I'm using some utils to simplify all the dirty work that we have to do in terms of checks and in particular I'm talking about the functions getNotNull, getOrNull and getType.

Let's see the code of these functions in details:

```kotlin
inline fun <reified T : Any> JsonObject.getNotNull(memberName: String): T =
    getOrNull(memberName)
        ?: throw JsonParseException("$memberName should be not null but is not present in the response or is null")

inline fun <reified T : Any> JsonObject.getOrNull(memberName: String): T? =
    when (T::class) {
        JsonObject::class -> toKotlinNullable(memberName)?.asJsonObject as? T
        JsonArray::class -> toKotlinNullable(memberName)?.asJsonArray as? T
        JsonPrimitive::class -> toKotlinNullable(memberName)?.asJsonPrimitive as? T
        String::class -> toKotlinNullable(memberName)?.asString as? T
        Boolean::class -> toKotlinNullable(memberName)?.asBoolean as? T
        Int::class -> toKotlinNullable(memberName)?.asInt as? T
        Long::class -> toKotlinNullable(memberName)?.asLong as? T
        else -> throw JsonParseException("Item type not supported")
    }

fun JsonObject.toKotlinNullable(memberName: String): JsonElement? =
    get(memberName)?.takeIf { !it.isJsonNull }

inline fun <reified T> getType(): Type = object : TypeToken<T>() {}.type
```

As you can see from the utils in the snippet, the only thing done is just to ensure that, the desired type of the field, is not null and, if yes, the field is extracted and cast to the specific requested type.

Now that we have the custom deserializers implemented, the last thing that we need to do is to register both of them in the `Gson` instance that we are using with `Retrofit`.

To do so, we can implement and use an extension function like this:

```kotlin
fun GsonBuilder.registerDeserializers() =
    registerTypeAdapter(PersonDTO::class.java, PersonDeserializer())
        .registerTypeAdapter(AddressDTO::class.java, AddressDeserializer())

val gsonInstance: Gson =
    GsonBuilder()
        .registerDeserializers()
        .create()
```

And that's it!
In this way, you can easily migrate every single DTO in a Kotlin friendly version in a separate PR without changing a lot of classes.

Once you are finished with the migration of all the DTO classes in your data layer, to start using `Kotlinx Serialization`, you just have to add the `@Serializable` annotation in all the DTO and replace the `Gson` instance used in Retrofit with `Kotlinx Serialization`.

All the custom deserializers that we implemented can be now deleted because no longer needed.

If you reached this step congratulations!
You successfully migrated your data layer to `Kotlinx Serialization` without (I hope) going crazy!

You can find all the provided code snippets in the following repository separated into different branches (one per step) as well as tests for all the custom deserializers.

{% github Harrypulvirenti/GsonMigration %}

Hope that this article helped you!

![gif](https://media.giphy.com/media/QLKSt3wQqlj7a/giphy.gif)