Unconfined Enums Adapter for Moshi

Isuru Rajapakse
2 min readMay 11, 2022

If you wonder what an Unconfined Enum is, wonder no more because it is a thing I just made up. Enums by definition are confined, or finite. Therefore “unconfined enum” is really an oxymoron.

Nevertheless, say you have this Fruit enum and want to map each value to an Android string resource. Kotlin’s enum properties make this convenient

enum class Fruit(@StringRes val stringRes: Int) {
Apple(R.string.fruit_apple),
Oranges(R.string.fruit_orange)
}

When we want to consume this enum from a Restful API with a Moshi converter, Moshi automatically generates the enum adapter for you, but if you want to customise the behaviour for whatever reason, you can do

object FruitsAdapter {
@ToJson fun toJson(type: Fruit): String = type.name
@FromJson fun fromJson(name: String): Fruit =
Fruit.values().first { it.name == name }
}

Happy days. Everything is working as expected.

When things go bananas

com.squareup.moshi.JsonDataException: Expected one of [Apple, Oranges] but was Bananas at path $

Good APIs don’t break contracts. Not all APIs are good APIs, so some can break contracts. What do we do now? push out an update with added enum value and its string resource? Perhaps we can make use of EnumJsonAdapter‘s .withUnknownFallBack()

enum class Fruit(@StringRes val stringRes: Int) {
Apple(R.string.fruit_apple),
Oranges(R.string.fruit_orange),
Unknown(R.string.fruit_unknown)
}

Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.add(Fruit::class.java, EnumJsonAdapter.create(Fruit::class.java)
.withUnknownFallback(Fruit.Unknown))
.build()

This certainly stops the app from crashing, but what if we actually want to show the “bananas” that the API sends? you know, as a fail safe so that users wouldn’t end up seeing “unknowns”.

Let's open up the Enums

Enums are by definition finite. Enums are final by design. Enums can’t be subclassed but they still can inherit interfaces. We will exploit this to create our “unconfined” enum

interface Fruit {
val name: String
data class Unknown(override val name: String) : Fruit
}
enum class Fruits(@StringRes val stringRes: Int) : Fruit {
Apple(R.string.fruit_apple),
Oranges(R.string.fruit_orange),
}

Our Fruit is no longer an Enum now. It is an interface, and you will need an adapter to tell Moshi how to convert your values back and forth.

object FruitsAdapter {
@ToJson fun toJson(type: Fruit): String = type.name
@FromJson fun fromJson(name: String): Fruit =
Fruits.values()
.find { it.name == name }
?: Fruit.Unknown(name)
}

Sure, we do end up losing our mapping to localised string resource if the server does end up sending down bananas (as expected), but this makes it convenient in handling bananas in the use site like

val formattedFruit: String = when (fruit) {
is Fruits -> getString(fruit.stringRes)
is Unknown -> fruit.name // not localised, but this is a fail-safe
}

Happy 🍌s

--

--