Forward compatible enums in Kotlin

A few years ago Adam outlined 8 steps to keep your API sane in his blog post which I really recommend if you haven’t seen it yet. The second step there, “be liberal in what the app accepts”, is quite a specific one because it is applicable not only to the backend side of the API but also, if not the most, the clients of that API.

I cannot stress enough the importance of that rule in the “offline first” apps. Let’s take a look at some example.

Example: 9GAG post tags

This is a screenshot where you can see a part of the tags list from the 9GAG Android app.

9GAG post tags list

At least some of these tags look like they could have the appropriate translations, e.g. the “Music” tag. I’m not saying 9GAG actually translates them, but it would be understandable if they did. In case of Android, we could use the string resource XML files for translations like this:

src/main/res/values/strings.xml

<resources>
    <string name="tag_music">Music</string>
    <string name="tag_art">Art</string>
</resources>

src/main/res/values-pl/strings.xml

<resources>
    <string name="tag_music">Muzyka</string>
    <string name="tag_art">Sztuka</string>
</resources>

Although there are many ways to implement the same thing, it seems quite reasonable to create an enum class for all the tags so that we can easily store the string resource identifiers with the translations there.

enum class PostTag(@StringRes val nameRes: Int) {
    MUSIC(R.string.tag_music),
    ART(R.string.tag_art)
}

Each 9GAG post could be described with these tag enums like this:

{
  "content": "Lorem ipsum...",
  "tags": ["MUSIC", "ART"]
}

The client app could parse these enums and show the appropriate translated tag names.

val postTagsNames = post.tags.map {
    context.getString(it.nameRes)
}

Forward incompatible enums

Let’s assume we use kotlinx.serialization library in the Android app to parse the JSON response bodies from the API. The default and simplest approach to enum serialization will probably look like this:

  • add @Serializable annotation to the PostTag enum class
  • the library will use the name property to serialize and deserialize enums

Let’s now imagine the backend developers extended the API by adding more post tags, e.g. a “Books” tag (encoded to string as "BOOKS"), while the Android app haven’t been updated yet. With the simple approach above, the app will most probably fail to deserialize the new and unknown enum value "BOOKS" if a post contains one. In such case, we will get the following exception:

kotlinx.serialization.SerializationException: com.example.9gag.posts.PostTag does not contain element with name 'BOOKS'

Fallback enum value

While searching for a solution to this problem, we may find other people implementing custom serializers with a dedicated “fallback enum value” like in this article. That approach requires us to add yet another enum value upfront and configure the serialization library to use FALLBACK value whenever it deserializes an unexpected string in JSON.

enum class PostTag(@StringRes val nameRes: Int) {
    MUSIC(R.string.tag_music),
    ART(R.string.tag_art),
    FALLBACK(R.string.tag_fallback)
}

As a result, the app will treat all the unknown enum values as the fallback value. I think it’s better than nothing as it allows the app to handle the unknown values somehow, e.g. by ignoring them, but it’s not perfect either.

First of all, if we want to just ignore the unknown values, it doesn’t make sense to create a dedicated string resource (like R.string.tag_fallback), but we have to do that because nameRes property is non-nullable. On the other hand, if we make it nullable to get rid of the unwanted resource, we will have to check the nullability every time we process the post tags which is unnecessarily burdensome.

There’s another one, even more important downside of the fallback enum value approach. If our app caches the data from the API but we overwrite the unknown values with said fallback value, we inevitably lose the original values the app received. If we later update the client app by adding the support for the BOOKS value, we will be forced to fetch the data from the API again in order to restore the BOOKS tag value in the post.

Of course, there are much more negative consequences possibly caused by losing the original enum values, especially if we consider other types of apps and features, but let’s focus on solving the problem first.

Forward compatibility of enums with codified

We came up with a better solution over a year ago, and it has been working really well since then. In short, instead of using a fallback enum values, we wrapped all the enums we send and receive from the APIs in a dedicated, generic sealed class allowing us to handle the unknown enum values very easily.

With codified library we get the ability to:

  • serialize and deserialize both known and unknown enum values
  • force the developers to handle both cases in
    exhaustive when expressions
  • preserve the unknown enum string codes (instead of overwriting them)
  • use custom string codes (i.e. different than enum names) for each enum value

In case of our exemplary PostTag enum class, all we need to do thanks to the codified library is:

  • implement Codified<String> interface in enum class
    enum class PostTag(
        override val code: String, // allows using a custom string instead of enum's name
        @StringRes val nameRes: Int
    ) : Codified<String> {
        MUSIC("MUSIC", R.string.tag_music),
        ART("ART", R.string.tag_art)
    }
    
  • declare the serializer object
    object PostTagCodifiedSerializer : KSerializer<CodifiedEnum<PostTag, String>> by codifiedEnumSerializer()
  • use the serializer wherever we want
    @Serializable
    data class Post(
        val content: String,
        @Serializable(with = PostTagCodifiedSerializer::class)
        val tags: List<CodifiedEnum<PostTag, String>>
    )
    

When we deserialize the Post object from JSON and access the tags property, we can:

  • map all the known values to their translated names:
    val knownPostTagsNames = post.tags
        .mapNotNull { it.knownOrNull() }
        .map { context.getString(it.nameRes) }
    
  • check if the particular enum value is known or not and handle all the possible cases
    val tag: CodifiedEnum<PostTag, String> // = ... get one from post.tags
    when (tag) {
        is CodifiedEnum.Known -> when (tag.value) {
            MUSIC -> TODO()
            ART -> TODO()
        }
        is CodifiedEnum.Unknown -> TODO()
    }
    
  • get the original string value: tag.code()

We can easily create the enum wrapper for any known PostTag: PostTag.MUSIC.codifiedEnum().

We can also create a wrapper for an unknown value and preserve the provided string code: "BOOKS".codifiedEnum<PostTag>()

Conclusion

If you want to keep your API sane and create offline first applications, you should consider the forward compatibility of your enums. Check out codified and give it a try – it will make your life much easier.

Note: codified version 1.1 uses kotlinx.serialization version 0.20.0. Support for version 1.0.0 and later is coming soon.

Image by Gerd Altmann from Pixabay

This article was originally published on Bright Inventions blog.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.