Blog Infos
Author
Published
Topics
,
Published

Moshi is one of the most popular JSON parsing libraries on Android that allows us to convert a JSON object into a data class and vice versa. It plays well with Retrofit, one of the most popular networking libraries, and since server responses are mostly represented in a JSON format, Moshi will save us from writing boilerplate code when parsing them on the client.

But for DRY-loving Android developers Moshi has a fatal flaw; enumerations. Out of the box, Moshi will require custom parsers for every enum in your API or it will require you to represent enums as Strings which no backend database will ever do.

Can we avoid repeating boilerplate code whilst still being able to parse enums without representing them as Strings? Of course we can (it’s software!), but the answer may surprise you.

Why Android developers love Moshi

Because all you need is an annotation. We have the following JSON representation of a Person which can be automatically converted into a data class, simply by adding the @JsonClass(generateAdapter = true) annotation. This annotation will generate an adapter at compile-time that will do the parsing for you.

{
"name": "John",
"age": 28
}
view raw person.json hosted with ❤ by GitHub

JSON representation of a Person

@JsonClass(generateAdapter = true)
data class Person(
val name: String,
val age: Int
)
view raw Person.kt hosted with ❤ by GitHub
Converting the JSON into a Person data class

For more complex data types, Moshi gives us the flexibility of writing our own custom adapters, essentially supporting all kinds of modeling that we wish to achieve.

Parsing enums with Moshi

Let’s add an additional property in our Person data class, which is Role. That would be an enum class with two possible values for now, Admin or Moderator.

enum class Role {
Admin,
Moderator
}
@JsonClass(generateAdapter = true)
data class Person(
val name: String,
val role: Role
)
view raw Person.kt hosted with ❤ by GitHub
Adding an enum class to the properties of Person

Now, what type of value do you expect in the JSON representation to map it into a Role? Is it a number or a String?

Moshi supports enum parsing by using the EnumJsonAdapter but it comes with a catch. The value in the JSON needs to be a String matching the name of the enum constant on the client. This obviously makes the code error-prone and harder to refactor, since by simply changing the name of an enum constant you will break the parsing.

fun createMoshi(): Moshi {
return Moshi.Builder()
.add(EnumJsonAdapter.create(Role::class.java)
.build()
}
view raw MoshiBuilder.kt hosted with ❤ by GitHub

Using Moshi’s EnumJsonAdapter to parse a Role

{
"name": "John",
"role": "Admin"
}
view raw person.json hosted with ❤ by GitHub
The value of role must match the name of the enum constant on the client; either Admin or Moderator
What if use numbers to represent the values instead?

Since representing the values as Strings is error-prone, what if we use plain integers that the client will parse and map them to the respective enum constant?

{
"name": "John",
"role": 1
}
view raw person.json hosted with ❤ by GitHub
The value of role is now represented as an integer

This makes our API less verbose and our client code easier to maintain since we no longer allow ourselves to break the parsing by simply renaming an enum constant.

However, now we can no longer use the EnumJsonAdapter that is provided by Moshi since the values are not represented by Strings. We will have to write a custom adapter that will map the integer into an enum constant.

First, let’s add a value property in our Role enum class that will match the one that we have defined in our JSON representation:

enum class Role(val value: Int) {
Admin(1),
Moderator(2);
companion object {
fun fromValueOrNull(value: Int): Role? {
return values().firstOrNull { it.value == value }
}
}
}
view raw Role.kt hosted with ❤ by GitHub
Associating an integer value with our Role enum class

As we can see in the snippet above, an Admin is now associated with a value of 1 while a Moderator is associated with a value of 2. The fromValueOrNull function will take the value and map it into an enum constant.

Let’s now write a custom adapter that will do the fromJson and toJson mapping:

class RoleEnumJsonAdapter : JsonAdapter<Role>() {
@FromJson
override fun fromJson(reader: JsonReader): Role? {
return if (reader.peek() != JsonReader.Token.NULL) {
Role.fromValueOrNull(reader.nextInt())
} else {
reader.nextNull()
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: Role?) {
writer.value(value?.value)
}
}
A custom Moshi adapter that will map an integer into a Role enum constant

We now managed to define enum classes as plain numbers in our server responses, rather than using Strings. But we did that at the cost of writing a custom adapter. What if we have dozens of enums with an associated value? Do we need to write all of that boilerplate code each time? Well that’s where generics come into play.

Creating a generic enum JSON adapter

First of all, we agree that we need to associate our enums with a number. That means that we’ll need to define an interface which has to be implemented by our enums:

interface IEnumValue {
val value: Int
}
enum class Role(override val value: Int) : IEnumValue {
Admin(1),
Moderator(2);
companion object {
fun fromValueOrNull(value: Int): Role? {
return values().firstOrNull { it.value == value }
}
}
}
view raw IEnumValue.kt hosted with ❤ by GitHub
Creating an IEnumValue interface to be implemented by our enums

To avoid now defining a fromValueOrNull function on every enum, we’ll take advantage of the interface we just created and we’ll create a GenericEnumFactory with a fromValueOrNull function that will take an integer and the type T of the enum and will do the mapping to return us the enum constant. The type parameter T will have of course to implement the IEnumValue interface:

enum class Role(override val value: Int) : IEnumValue {
Admin(1),
Moderator(2)
}
object GenericEnumFactory {
inline fun <reified T> fromValueOrNull(rawValue: Int?): T? where T : Enum<T>, T : IEnumValue {
return enumValues<T>().firstOrNull { it.value == rawValue }
}
}
class RoleEnumJsonAdapter : JsonAdapter<Role>() {
@FromJson
override fun fromJson(reader: JsonReader): Role? {
return if (reader.peek() != JsonReader.Token.NULL) {
GenericEnumFactory.fromValueOrNull(reader.nextInt())
} else {
reader.nextNull()
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: Role?) {
writer.value(value?.value)
}
}
Creating a GenericEnumFactory to avoid specifying a fromValueOrNull function on each of our enums

We just managed to remove some boilerplate code from our enums, but we still have to define a new JsonAdapter per enum. Could we somehow define only one adapter and reuse it across all our enums?

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

The magic of reified type parameters

If we want to create a generic adapter, we will need access to the type T of the enum so that we can access the enum constants and their value that the IEnumValue interface provides. But we know that the type is erased at runtime and only available at compile time. That’s exactly where the reified type parameters come in — they will allow us to access a type passed as a parameter as if it was a normal class.

However at the time of writing this post, reified type parameters are not supported in class level in Kotlin, therefore any attempt to create a generic adapter like the following would fail:

class GenericEnumJsonAdapter<T> : JsonAdapter<T>() {
@FromJson
override fun fromJson(reader: JsonReader): T? {
// Cannot access enumValues<T>()
}
@ToJson
override fun toJson(writer: JsonWriter, value: T?) {
// ...
}
}
Enum values cannot be accessed unless T is reified which is not supported in class level

There’s another trick we can do though. We know that reified type parameters are supported in inline functions, so instead of defining a generic class, what if we define a generic function that will return the generic JsonAdapter<T>? And that will be a perfectly valid solution!

inline fun <reified T> createEnumJsonAdapter(): JsonAdapter<T> where T : Enum<T>, T : IEnumValue {
return object : JsonAdapter<T>() {
@FromJson
override fun fromJson(reader: JsonReader): T? {
return if (reader.peek() != JsonReader.Token.NULL) {
enumValues<T>().firstOrNull { it.value == reader.nextInt() }
} else {
reader.nextNull()
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: T?) {
writer.value(value?.value)
}
}
}
 generic function that returns the generic JsonAdapter<T>

There we have a generic function that will auto-generate for us the enum adapter to do the fromJson and toJson mapping, and that supports the integer representation of our enums in the server responses!

Note: firstOrNull as hinted by its name can return null if the integer value we provide cannot be mapped into an enum constant. This means that the enums in the data classes that model our server responses have to be nullable. We have the option though to define a default value, for example by returning enumValues<T>().first() if firstOrNull returns null.

Usage of this function is pretty simple, we just need to add the adapters in our Moshi.Builder definition, similarly as we would do for any other custom Moshi adapter:

fun createMoshi(): Moshi {
return Moshi.Builder()
.add(createEnumJsonAdapter<Role>())
.add(createEnumJsonAdapter<Category>())
.add(createEnumJsonAdapter<AnotherEnum>())
// ...
.build()
}
view raw MoshiBuilder.kt hosted with ❤ by GitHub
Adding the custom adapter for our enums in our Moshi.Builder definition

This approach allows us to represent enums as integers in the server responses without us having to write and maintain a separate adapter for each of our enum classes. At the same time, the code becomes easier to maintain and refactor, and more resilient to bugs.

About the authors

Lucas Cavalcante and Stelios Frantzeskakis are developers for Perry Street Software, publishers of the LGBTQ+ dating apps SCRUFF and Jack’d, with more than 30M members worldwide.

Thanks to Stelios Frantzeskakis

 

This article was originally published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Modern mobile applications are already quite serious enterprise projects that are developed by hundreds…
READ MORE
blog
Kotlin Symbol Processing (KSP) is a new API from Google for creating lightweight Kotlin…
READ MORE

Leave a Reply

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

Fill out this field
Fill out this field
Please enter a valid email address.

Menu