Authors Top

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

1. Introduction

An enumeration is a finite set of ordered values representing a domain or a collection. Enumerations, or Enums, are very widely used in many programming languages to model the domain. Scala also supports enumerations. However, there are some practical problems with Scala’s in-built enum type. In this tutorial, we’ll take a look at Enumeratum, an improved enumeration library for Scala.

2. Existing Ways to Create Enums

Let’s take a look at how enumerations are currently created in Scala.

2.1. Using Enumeration Keyword

The in-built method to create enumerations in Scala is done by extending with the abstract class Enumeration. However, there are some major issues with this approach, such as:

  • The JVM erases Enumerations at run-time
  • No exhaustive check for pattern matching at compile time

2.2. Using Algebraic Data Type (ADT)

Due to the issues with Scala’s Enumeration, enums are often created as ADTs using sealed traits and case objects. This solves some of the issues, but there are still some drawbacks to this approach. Some drawbacks include:

  • No out-of-the-box solution to build and list enumerations
  • No automatic ordering for the items
  • Need to write custom logic for serialization and deserialization

3. Enumeratum

The Enumeratum library tries to solve all the previously mentioned issues. Some of the advantages of using Enumeratum are:

  • Exhaustive pattern-matching warning
  • Very fast and easy to use
  • Many in-built utility methods to customize enumerations
  • Very good integration with popular JSON and database libraries

3.1. Setup

To use Enumeratum, we first need to add the library dependency:

libraryDependencies ++= Seq(
    "com.beachape" %% "enumeratum" % "1.7.0"
)

Then, we can import enumeratum._ in our code and the required classes will be available in scope.

3.2. Defining a Simple Enum

Let’s look at how we can define a simple enum. We’ll be using the types EnumEntry and Enum to define our enumerations. First, we’ll create a sealed trait, or abstract class, to define our enum type. For this, we’ll extend the trait EnumEntry:

sealed trait Country extends EnumEntry

Now, let’s define a list of countries:

object Country extends Enum[Country] {
  case object Germany extends Country
  case object India extends Country
  override val values: IndexedSeq[Country] = findValues
}

Notice that we’re implementing the values field, with the list of possible values. Enumeratum provides a method findValues that returns all the defined enum elements in the same order in which they’re defined. Let’s verify the above implementation:

val countries = Country.values
assert(countries.size == 2)
assert(countries.equals(Seq(Country.Germany, Country.India)))

3.3. Utility Methods

Enumeratum provides many utility methods on enum values. Let’s look at some popular and useful ones. We can convert a String value to enum using the withName() method:

Country.withName("India")

If the value is not a valid enum name, it will throw a NoSuchElementException at runtime. To handle such cases, we can use the withNameOption method:

val usa: Option[Country] = Country.withNameOption("USA")

This will return None if the value passed is an invalid enum name. Otherwise, it will be wrapped in Some. Additionally, there are methods to handle case sensitivity:

val germanyCaps = Country.withNameInsensitive("GERMANY")
assert(germanyCaps == Country.Germany)
val indiaMixed = Country.withNameInsensitive("iNDia")
assert(indiaMixed == Country.India)
val usaInsensitive = Country.withNameInsensitiveOption("uSA")
assert(usaInsensitive.isEmpty)

3.4. Customizing Enum Values Using override

By default, the enum value will be the same as the case object name. However, we can customize this name by overriding the entryName parameter:

sealed abstract class Gender(override val entryName: String) extends EnumEntry
object Gender extends Enum[Gender]{
  override def values: IndexedSeq[Gender] = findValues
  case object Male extends Gender("M")
  case object Female extends Gender("F")
  case object Other extends Gender("O")
}

Now, the enum values will be overridden to M, F, and O instead of Male, Female, and Other:

val male = Gender.withName("M")
assert(male == Gender.Male)

3.5. Customizing Enum Values Using Utility Traits

Instead of overriding the enum values explicitly, Enumeratum provides a set of utility traits for common scenarios. We can use these traits to extend our enum entries. These traits will internally override the entryName parameter with some of the common String conversions. Let’s look at an example:

sealed trait NamingConvention extends EnumEntry with EnumEntry.LowerCamelcase
object NamingConvention extends Enum[NamingConvention] {
  override val values: IndexedSeq[NamingConvention] = findValues
  case object JavaStyle extends NamingConvention
  case object ScalaStyle extends NamingConvention
  case object PythonStyle extends NamingConvention with EnumEntry.Snakecase
}

The trait NamingConvention is extending a trait EnumEntry.LowerCamelcase, which will modify the entryName parameter. This is equivalent to explicitly defining a JavaStyle enum as:

sealed abstract class NamingConvention(override val entryName: String) extends EnumEntry 
object NamingConvention extends Enum[NamingConvention] {
  override val values: IndexedSeq[NamingConvention] = findValues
  case object JavaStyle extends NamingConvention("javaStyle")
  case object ScalaStyle extends NamingConvention("scalaStyle")
  case object PythonStyle extends NamingConvention("python_style")
}

3.6. Defining Value Enums

Apart from the regular String-based enums, Enumeratum also supports Int, Long, Char, and so on. Instead of extending with EnumEntry, we need to use the more specific type like IntEnumEntry, LongEnumEntry, and so on:

import enumeratum.values._
sealed abstract class HttpCode(val value:Int, val name:String) extends IntEnumEntry
object HttpCode extends IntEnum[HttpCode] {
  override val values: IndexedSeq[HttpCode] = findValues
  case object OK extends HttpCode(200, "Ok")
  case object BadRequest extends HttpCode(400, "Bad Request")
}

Now, we can create the enum using the integer value:

val bad = HttpCode.withValue(400)
assert(bad == HttpCode.BadRequest)
assert(bad.name == "Bad Request")

4. Integration with Other Libraries

Enumeratum has very good integration with other libraries such as JSON libraries and database libraries. Some of the supported libraries are Json4s, Play, Slick, ReactiveMongo, and Circe.

5. Conclusion

In this article, we looked at the drawbacks of enumerations in Scala and how we can use Enumeratum to handle enums in a better way. As always, the sample code used in this tutorial is available over on GitHub.

Authors Bottom

We’re starting a new Scala area. If you have a few years of experience in the Scala ecosystem, and you’re interested in sharing that experience with the community, have a look at our Contribution Guidelines.

Comments are closed on this article!