---
title: "Migration: Gson to Kotlinx.Serialization"
source: "/writing/migration-gson-to-kotlinx-serialization/"
---

Article

# Migration: Gson to Kotlinx.Serialization

David Perez

October 4, 2023

There are plenty of apps out there that are old enough to have been written in Java originally, even if they aren't anymore. This means that certain things, even if they were the right tool at the time, are no longer the correct tool. Even if the entire app has migrated to Kotlin, there is still a good chance that no one ever bothered to migrate off of [Gson](https://github.com/google/gson) since it was working well enough. Eventually, you might find yourself trying to tackle that last bit of technical debt in the project and migrate to the newer tools. And this is where we begin.

### The Models

Before you even introduce the [Kotlin.Serialization](https://github.com/Kotlin/kotlinx.serialization) library, you should begin looking at the transition from Java network models to Kotlin network models.

-   **Network Request Models** tend to be much easier since they are instantiated programmatically and all the usual Kotlin requirements will be handled.
-   **Network Response Models** are where things can be trickier than you think because Gson is building the objects via reflection and using unsafe constructors. Making sure you are not using default parameters, delegates, or the init block is important since none of those things will work while you are still using Gson. If you are curious about why those things do not work or would like a [handy library](https://github.com/livefront/gson-kotlin-adapter) to bridge the gap during this transition, I suggest you take a look at my previous article [here](/writing/adapting-gson-to-kotlin) .

Because of these limitations, it is important to keep your models simple while still using Gson; think basic properties and functions. Something like this:

data class ResponseModel(
    @SerializedName("string_data") val stringData: String,
    @SerializedName("int_data") private val _intData: Int?,
) {
    val intData: Int get() = _intData ?: 15

    fun getAllData(): String {
        "$stringData_$intData"
    }
}

I find that the hardest thing to avoid when using Gson is default parameters. Often times I find that I want there to be a value prescribed when the network response does not provide one, and the example above shows a simple way to make that work. This approach also works perfectly fine once the transition to Kotlinx.Serialization is complete, though it is a little verbose.

Once you have completely migrated to Kotlinx.Serialization, you can finally update your models to use idiomatic Kotlin and use true default values. Make sure you enable the `encodeDefaults` feature, otherwise it will not serialize the property when it is set to the default value.

Json {
    encodeDefaults = true
}

@Serializable
data class ResponseModel(
    @SerialName("string_data") val stringData: String,
    @SerialName("int_data") val intData: Int = 15,
) {
    fun getAllData(): String {
        return "$stringData_$intData"
    }
}

There are three changes that have been made to the model to work with Kotlinx.Serialization.

-   **Serializable:** The`@Serializable` annotation is now added to the model.
-   **Serial Name:** All usages of the `@SerializedName` annotation is replaced with the `@SerialName` annotation.
-   **Default Arguments:** A default value for the `intData` is added to the constructor to simplify the model.

### The Adapters

Once you've migrated all the models, you need to look at any custom adapters that you may have introduced. For the average project, this is not terribly difficult to migrate as there are similar tools in Kotlinx.Serialization. The `[KSerializer](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/)` is a nice replacement for Gson's `[TypeAdapter](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/TypeAdapter.html)` . It provides you with an `[Encoder](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/)` and `[Decoder](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-decoder/)` which has similar functionality to the `[JsonWriter](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonWriter.html)` and `[JsonReader](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/stream/JsonReader.html)` . For the purposes of this content, we will assume that you are reading and writing JSON, so it will always be a `[JsonEncoder](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-encoder/)` and `[JsonDecoder](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-decoder/)` .

For the simplest of cases, the differences in code will be minimal between Gson and Koltinx.Serialization. Let's start with a little JSON representing some dimensions:

{
  "dimens": "10x15",
  "unit": "ft"
}

Now we want to parse this into a model that includes the width and length as two separate properties, something like this:

data class Area(
    @SerializedName("dimens") val dimension: Dimension,
    @SerializedName("unit") val unit: String,
)

data class Dimension(
    @SerializedName("length") val length: Int,
    @SerializedName("width") val width: Int,
)

To accomplish this sort of task in Gson, we might have an adapter that looks like this:

class DimensionTypeAdapter : TypeAdapter<Dimension>() {
    override fun read(reader: JsonReader): Dimension {
        val dimensionsList = reader.nextString().split("x")
        return Dimension(
            width = dimensionsList[0].toInt(),
            length = dimensionsList[1].toInt(),
        )
    }

    override fun write(writer: JsonWriter, value: Dimension) {
        writer.value("${value.width}x${value.length}")
    }
}

Once converted to a `KSerializer`, the code is almost identical:

class DimensionSerializer : KSerializer<Dimension> {
    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor(
            serialName = "Dimension",
            kind = PrimitiveKind.STRING,
        )

    override fun deserialize(decoder: Decoder): Dimension {
        val dimensionsList = decoder.decodeString().split("x")
        return Dimension(
            width = dimensionsList[0].toInt(),
            length = dimensionsList[1].toInt(),
        )
    }

    override fun serialize(encoder: Encoder, value: Dimension) {
        encoder.encodeString("${value.width}x${value.length}")
    }
}

Just be sure to update the model appropriately to associate the new serializer with the model:

@Serializable
data class Area(
    @SerialName("dimens") val dimension: Dimension,
    @SerialName("unit")val unit: String,
)

@Serializable(with = DimensionSerializer::class)
data class Dimension(
    @SerialName("length") val length: Int,
    @SerialName("width") val width: Int,
)

Just as before, the model now uses the `@Serializable` and `@SerialName` annotations. The bigger difference is that we have added the `DimensionSerializer` to the `@Serializable` annotation indicating that this model should always be serialized with the customer serializer.

In some cases, you may have to serialize a class that you cannot directly annotate. This is usually because the class in question is from a library. A common example might be the Java `ZonedDateTime` class. For something like this, you would still need to make a custom `KSerializer` but because you cannot annotate the class, you must annotate the properties with `@Contextual` instead:

Json {
    serializersModule = SerializersModule {
        contextual(ZonedDateTime::class, ZonedDateTimeSerializer())
    }
}

@Serializable
data class CustomTimeClass(
    @Contextual @SerialName("datetime") val time: ZonedDateTime,
)

Note that there is more than just the annotation: we also needed to add the serializer to the `serializersModule`. This works fairly well but if you prefer to keep your `Json` class empty, you can specify the exact serializer on the property by replacing the `@Contextual` annotation with `@Serialization(with = ZonedDateTimeSerializer::class)`.

### JSON Manipulation

Sometimes the JSON you get is not quite the style you want it to be in. Or perhaps the JSON is just inconsistent and you want to be able to handle the diversity in one place. There is a solution for that too: the `JsonTransformingSerializer` is specifically made as an easy way to handle this sort of problem.

For example, here is an array of places and their associated location (using coordinates). The location is what we care about here; notice that it can be represented in a variety of different formats, which makes it less than desirable to work with:

[
  {
    "name": "home",
    "location": null
  },
  {
    "name": "office",
    "location": [40.737508, -73.937633]
  },
  {
    "name": "cafe",
    "location": {
      "latitude": 40.378051,
      "longitude": -73.336739
    }
  }
]

When using Gson, you may have created a custom serializer that implemented both the `[JsonDeserialzer](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/JsonDeserializer.html)` and the `[JsonSerializer](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/JsonSerializer.html)` interfaces. This granted you direct access to the `[JsonElement](https://www.javadoc.io/doc/com.google.code.gson/gson/latest/com.google.gson/com/google/gson/JsonElement.html)` where you could then construct the model as you saw fit:

class LocationSerializer : JsonSerializer<Location>, JsonDeserializer<Location> {
    override fun deserialize(
        json: JsonElement?,
        typeOfT: Type?,
        context: JsonDeserializationContext?,
    ): Location = when (json) {
        is JsonObject -> Location(
            latitude = json.get("latitude").asDouble,
            longitude = json.get("longitude").asDouble,
        )
        is JsonArray -> if (list.size == 2) {
            Location(
                latitude = json[0].asDouble,
                longitude = json[1].asDouble,
            )
        } else {
            throw IllegalArgumentException(
                "Invalid JSON format for Location",
            )
        }
        else -> throw IllegalArgumentException(
            "Invalid JSON format for Location",
        )
    }

    override fun serialize(
        src: Location,
        typeOfSrc: Type?,
        context: JsonSerializationContext,
    ): JsonElement = JsonArray(2).apply {
        add(src.latitude)
        add(src.longitude)
    }
}

The above serializer gives us a chance to directly translate the `Location` object to and from JSON elements. We can translate the above code into a `[JsonTransformingSerializer](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-transforming-serializer/)` which functions in a similar manner:

class LocationTransformingSerializer : JsonTransformingSerializer<Location>(
    tSerializer = Location.serializer(),
) {
    override fun transformDeserialize(
        element: JsonElement,
    ): JsonElement = when (element) {
        is JsonObject -> element
        is JsonArray -> if (element.size == 2) {
            JsonObject(
                mapOf(
                    "latitude" to element[0],
                    "longitude" to element[1],
                ),
            )
        } else {
            throw IllegalArgumentException(
                "Invalid JSON format for Location",
            )
        }
        else -> throw IllegalArgumentException(
            "Invalid JSON format for Location",
        )
    }

    override fun transformSerialize(
        element: JsonElement,
    ): JsonElement = when (element) {
        is JsonArray -> element
        is JsonObject -> JsonArray(
            listOf(
                element.getValue("latitude"),
                element.getValue("longitude"),
            )
        )
        else -> throw IllegalArgumentException(
            "Invalid JSON format for Location",
        )
    }
}

The biggest difference between the old code and this new serializer is that we never actually deal with a `Location` object. They both start and end with a `[JsonElement](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-element/)` ; we inspect the `JsonElement` to determine what type of data we have received and then transform it into the structure we want. Once the serializer is ready, just annotate models:

@Serializable
data class Building(
    @SerialName("name") val name: String,
    @Serializable(LocationTransformingSerializer::class)
    @SerialName("location") val location: Location?,
)

@Serializable
data class Location(
    @SerialName("latitude") val latitude: Double,
    @SerialName("longitude") val longitude: Double,
)

This solution is fine but requires you to annotate each spot in the app that needs to use this custom serializer. Unfortunately, you cannot apply the serializer to the class itself (like we did with the `Dimension` class) because we are referencing the default-generated serializer when constructing the `LocationTransformingSerializer`. To override the serializer for the entire class, we would need to create a custom serializer for the `Location` class and pass that into the `JsonTransformingSerializer`.

### Polymorphism

Handling polymorphic JSON in Gson can be a bit of a hassle because there is nothing included in the library for handling it. You always need to write a custom adapter in order to handle the unique payloads or copy the `[RuntimeTypeAdapterFactory](https://github.com/google/gson/blob/main/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java)` , which still requires additional setup. Take this JSON as an example:

[
  {
    "type": "feed_message",
    "id": 1234,
    "message": "Hello!"
  },
  {
    "type": "feed_icon",
    "id": 1234,
    "icon_url": "www.exampleicon.com"
  },
  {
    "type": "feed_bonus",
    "id": 1234,
    "bonus": "bonus!!!"
  }
]

In order to parse this, you would need to create a custom `JsonDeserializer` that can identify the unique payloads and defer to the correct deserializer. It ends up being fairly simple but more of a hassle than you want it to be.

class FeedItemSerializer : JsonDeserializer<FeedItem>, JsonSerializer<FeedItem> {
    override fun deserialize(
        json: JsonElement,
        typeOfT: Type?,
        context: JsonDeserializationContext,
    ): FeedItem {
        val jsonObject = json.asJsonObject
        val feedItemType = jsonObject.get("type").asJsonPrimitive.asString
        return when (feedItemType) {
            "feed_bonus" -> context.deserialize<FeedItem>(
                jsonObject,
                FeedBonus::class.java,
            )
            "feed_icon" -> context.deserialize<FeedItem>(
                jsonObject,
                FeedIcon::class.java,
            )
            "feed_message" -> context.deserialize<FeedItem>(
                jsonObject,
                FeedMessage::class.java,
            )
            else -> throw IllegalArgumentException(
                "Invalid JSON format for FeedItem",
            )
        }
    }

    override fun serialize(
        src: FeedItem,
        typeOfSrc: Type?,
        context: JsonSerializationContext,
    ): JsonElement = when (src) {
        is FeedBonus -> context.serialize(src, FeedBonus::class.java)
        is FeedIcon -> context.serialize(src, FeedIcon::class.java)
        is FeedMessage -> context.serialize(src, FeedMessage::class.java)
    }
}

This can be used to construct models like this:

sealed class FeedItem {
    abstract val id: Int
    abstract val type String

    data class FeedBonus(
        @SerializedName("id") override val id: Int,
        @SerializedName("type") override val type: String,
        @SerializedName("bonus") val bonus: String,
    ) : FeedItem()

    data class FeedIcon(
        @SerializedName("id") override val id: Int,
        @SerializedName("type") override val type: String,
        @SerializedName("icon_url") val url: String,
    ) : FeedItem()

    data class FeedMessage(
        @SerializedName("id") override val id: Int,
        @SerializedName("type") override val type: String,
        @SerializedName("message") val message: String,
    ) : FeedItem()
}

There 3 main tools for handling polymorphism in Kotlinx.Serialization; each has some advantages and disadvantages depending on your JSON modeling.

-   **Class Discriminator:** This is a global property that can be found on the `Json` class; it allows you to denote which class to deserialize based on the specified property in the JSON. It is extremely easy to use and requires _almost_ no setup. The primary limitation is that there is only one `classDiscriminator`, which can prove very problematic when dealing with inconsistent JSON models.
-   **Json Class Discriminator:** Extremely similar to the `classDiscriminator` but provides the solution to its main limitation. This is an annotation that can be used to specify a different property to be used when determining which class to deserialize.
-   **Json Content Polymorphic Serializer:** This type of custom `KSerializer` is going to look the closest to a `JsonDeserializer` you may be accustomed to writing for Gson. It provides a function, `selectDeserializer`, that allows you to inspect the JSON and determine which serializer should be used based on any arbitrary data you define.

Using the `classDiscriminator` and `@JsonClassDiscriminator` can remove the need for custom serializers completely but they only work when you have some sort of `type` property to key off of. The `JsonContentPolymorphicSerializer` is much more flexible since you can determine the correct serializer from any arbitrary data but does require you to create a new serializer.

The usage of Polymorphism is complex enough to warrant its own article, so I made one. For greater detail and examples, please [check it out!](https://medium.com/@david37/b8f5f1cedc99)

### The End

With these tools and ideas in hand, you should be able to handle the _most_ common issues that could be run into while migrating to Kotlinx.Serialization.

_David deserialized all sorts of models at_ [_Livefront_](https://livefront.com/)

-   David Perez

    Software Engineer

    [More from David](/team/david-perez/)

    David Perez

### Tags

[Digital Engineering](/insights/all/?topic=digital-engineering) [Mobile Apps](/insights/all/?topic=mobile-apps)

### Share

-   Copied

## View these next.

[See all](/insights/all/?type=writing)

-   [

    Article

    How To Sabotage Your Project Using Inconsistency

    Collin Flynn

    ](/writing/how-to-sabotage-your-project-using-inconsistency/)
-   [

    Article

    Unit Testing race conditions by creating chaos (Swift)

    Sean Berry

    ](/writing/unit-testing-race-conditions-by-creating-chaos-swift/)
-   [

    Article

    UIApplicationDelegate call sequence reference

    Sean Berry

    ](/writing/uiapplicationdelegate-call-sequence-reference/)
