NELKINDA SOFTWARE CRAFT

A Type-Safe UUID in Kotlin

TL;DR: Create a value class with a type parameter, and use it everywhere except on the JPARepository.

Author:
Christian Hujer, CEO / CTO at Nelkinda Software Craft Pvt Ltd
First Published:
by NNelkinda Software Craft Private Limited
Last Modified:
by Christian Hujer
Approximate reading time:

A type-safe UUID, that would be nice, right? In Kotlin, I've done it! Here's how!

1 Motivation

Identifiers are everywhere. Identifiers help us to identify things by reference without passing a copy of the entire thing around. One of the most popular identifiers is the UUID, available in Java through the java.util.UUID class.

But this class has one problem. The UUID does not know what it identifies. Say you write Spring code that books a cinema ticket. You might end up having a method that looks a bit like this: fun bookSeat(movie: UUID, user: UUID) Whether the first UUID identifies a movie or a user is not clear at the call-site. Point of this example is, that the UUIDs are not type-safe. It's easy to get them mixed up, and the compiler will not help you. Unless…

2 Behold: Type-safe UUIDs!

In Kotlin, we can use a parameterized value class to create a type-safe UUID. Here's how the code could look like:

@Suppress("unused") // TargetType used for type-checking
@JvmInline
value class UUID<TargetType>(val value: java.util.UUID) {
    companion object {
        @JvmStatic
        fun <TargetType> fromString(name: String) =
            UUID<TargetType>(java.util.UUID.fromString(name))

        @JvmStatic
        fun <TargetType> randomUUID() =
            UUID<TargetType>(java.util.UUID.randomUUID())
    }
}
Listing 2-1: Listing: An example for a type-safe UUID class

How to use it? Simply use it everywhere except at your JPARepository. Why? Because Kotlin value classes and Hibernate don't play well together (yet?) in all situations.

Here's how to use it in an Entity:

@Entity
data class Movie {
    @Id
    @get:JvmName("getId")
    val id: UUID<Movie>,
    // ...
}
Listing 2-2: Example for how to use the type-safe UUID on an Entity

Note the @get:JvmName("getId") annotation. It is important to have a getter with the name getId for Hibernate to work properly. Currently, usages of Kotlin value classes are always name-mangled unless the name is explicitly mentioned.

Here's how to (not) use it in a Repository:

interface MovieRepository : JpaRepository<Movie, java.util.UUID>
Listing 2-3: Example for how to use the type-safe UUID on a Repository: Don't, instead use java.util.UUID directly

Note that the repository uses java.util.UUID, not our new type-safe UUID type. Hibernate has no support for Kotlin value-classes yet, and will not be able to find the entity by the type-safe UUID.

And here's how to use it in a Service:

@Service
class MovieService(
    @Autowired private val movieRepository: MovieRepository,
) {
    fun findById(id: UUID<Movie>) = movieRepository.findById(id.value)
}

In the service, we "translate" from the type-safe UUID to the "normal" java.util.UUID of the repository that Hibernate understands.

It should be noted that the type-safe UUID also works on @RestControllers and in Swagger without any issues.

3 Conclusion

Kotlin value classes are a great way to create type-safe UUIDs. Note that the support in Hibernate and elsewhere might not be perfect yet. Hence, the workaround to directly use java.util.UUID on the JPARepository.