Using Catatumbo with Kotlin

Catatumbo is a great ORM framework for using Java with Google Cloud Datastore. It abstracts away Google’s clunky Datastore libraries and enables deserialization directly into Kotlin objects.

If you’re like me and love Kotlin you’ll quickly hit some issues though.

The Problem

Let’s say you have a pretty simple model to store users:

data class User(
  @Identifier
  val id: Long = 0,

  // We don't wants users to be able to change their email
  val email: String,
  var isAdmin: Boolean = false,

  // null when we haven't prompted the user yet
  var favoriteFood: String?
)

Full code

Running this will give us an error though:

com.jmethods.catatumbo.UnsupportedConstructionStrategyException: Class User requires a public no-arg constructor or a public static method that returns a corresponding Builder instance. The name of the static method can either be newBuilder or builder

This is a great error message in that it tells us what it was expecting and a way to fix it. But first, let’s dig into why Catatumbo has these expectations in the first place.

How Catatumbo Works

When we insert our User object Catatumbo does the following:

  1. Creates a new User instance by calling User() with no arguments (it doesn’t reuse the User we passsed in)
  2. Populates each field of User using setters (e.g. setId())
  3. Returns the User instance.

We’re failing on the first step here. User doesn’t have a constructor that takes no arguments. A Kotlin data class only has that when all the fields have default values.

Quick&Dirty Solution

Just adding default values to User won’t be enough though. When Catatumbo tries to overwrite val fields like email or id it will fail because those are supposed to be immutable.

To make our data class fully compatible with Catatumbo we need to make every field mutable and have a default value:

data class User(
  @Identifier
  var id: Long = 0,

  var email: String? = null,
  var isAdmin: Boolean = false,
  var favoriteFood:String? = null
)

Full code

This works but we’ve lost two of Kotlin’s awesome features: immutability and non-null types. Now we need to rely on developers to remember which fields are supposed to be immutable and which ones are non-nullable.

There is a better way to do this though!

The Better Solution: Builders

There is a way to deal with immutable and non-nullable fields without losing Kotlin’s advantages: the builder pattern. When inserting or loading Catatumbo does the following:

  1. If User has public static User builder() or public static User getBuilder() then continue, otherwise try to user setters
  2. Call the builder() method to create a new UserBuilder
  3. For each field find a method to set it. The allowed patterns for these method names are property(), setProperty(), or withProperty()
  4. Call .build() to return a new User

So let’s see this in action:

data class User(
  @Identifier
  val id: Long = 0,

  val email: String,
  var isAdmin: Boolean = false,
  var favoriteFood: String?
) {
  companion object {
    @JvmStatic
    fun builder(): UserBuilder = UserBuilder()
  }
}

class UserBuilder {
  var id: Long = 0

  var email: String? = null
  var isAdmin: Boolean? = false
  var favoriteFood: String? = null

  fun build(): User {
    return User(
      id = id,
      email = email!!,
      isAdmin = isAdmin!!,
      favoriteFood = favoriteFood
    )
  }
}

Full Code

User’s fields are exactly the same as they were in the beginning. The only difference in the data class is that we’ve added a companion object that returns a builder. We need to annotate it with @JvmStatic so that it gets generated as a public static method that Catatumbo recognizes.

Our UserBuilder class looks a lot like the quick&dirty solution shown in the previous section. To get a no-arg constructor and allow mutation every property is a var and has a default value.

You might be tempted to keep fields like email non-nullable (e.g. var email:String = ""). This comes with risks though. If an entity in Datastore doesn’t have email set and then when we load it, the instance of Person will set email to an empty string rather than erroring.

For production we’ll likely want to remove all the !! non-null assertions and have more advanced error checking but this works to demonstrate our point.

The builder pattern isn’t perfect though. It’s mostly boilerplate and we now need to keep default values synchronized between the data class and the builder. It is on my todo list to experiment with Kotlin code generation using KotlinPoet to create these builders automatically though.

Conclusion

Catatumbo is a great library that I highly recommend using to make your life easier when dealing with Google Cloud Datastore. With a little bit of work you can get it working with Kotlin.

Have a different way of using Catatumbo with Kotlin? I’d love to hear! Get in touch with me on Twitter or LinkedIn.

Thanks to David Justo for reviewing the draft of this post!