Learning enumeratum, part 1: why not just use scala.Enumeration?
At work, the Scala codebases I touch use enumeratum almost everywhere a value comes from a fixed set: statuses, channels, currencies, event types. I'd read it and used it without ever asking why we don't just use what Scala gives us. This series is me fixing that, one small lesson at a time, with runnable code.
All the code lives in a public repo: zakariahere/learning-enumeratum. Each lesson is its own package. This first one is lesson1. The setup is Scala 2.13.18, sbt 1.11.7 and enumeratum 1.9.0.
The setup
ThisBuild / scalaVersion := "2.13.18" libraryDependencies += "com.beachape" %% "enumeratum" % "1.9.0"
That one dependency is the core library. The integrations (circe, play-json, doobie and so on) are separate modules, and I'll get to them later in the series.
Option 1: scala.Enumeration
This is the enum that ships with the standard library:
object ColorEnum extends Enumeration {
type ColorEnum = Value
val Red, Green, Blue = Value
}
It's short, and you get values and withName for free. The problem is the type: Red, Green and Blue are all just ColorEnum.Value. A value can't have its own type, its own fields or its own methods without awkward workarounds. withName also throws NoSuchElementException on unknown input, and you can't ask it for an Option.
I expected the compiler to stay silent when a match missed a case. I wrote one that left out Blue to prove it, and Scala 2.13 surprised me:
[warn] PlainScala.scala:32:44: match may not be exhaustive.
[warn] It would fail on the following input: (x: lesson1.ColorEnum.ColorEnum forSome x not in (Green, Red))
So on 2.13, a match on Enumeration that misses a case does get a warning. Scala 2.12 and earlier don't do this, and it's still only a warning about one shared type. The rest of the problems remain.
Option 2: sealed trait and case objects
This is the idiomatic ADT way, and what most Scala developers reach for first:
sealed trait Color
object Color {
case object Red extends Color
case object Green extends Color
case object Blue extends Color val values: List[Color] = List(Red, Green, Blue)
def fromString(s: String): Option[Color] = values.find(_.toString == s)
}
Every value now has its own type, the compiler checks every match covers all cases, and each value can carry fields and methods. What's left is chores. values is a list I keep up to date by hand. If I add Yellow and forget the list, nothing fails to compile, and every lookup quietly ignores the new value. String parsing is code I'd have to write again for every enum in the codebase.
Option 3: enumeratum
enumeratum keeps option 2 as it is and removes the chores:
![An enumeratum enum: sealed trait Fruit extends EnumEntry, a companion object extending Enum[Fruit] with values = findValues, three case objects, and the withNameOption and withNameInsensitiveOption calls](/blog/media/c6e2d7539804dbd716fcfac572cfd728968bf0f913a4bf8df1f717b29f4aec4c.png)
Two changes: the trait extends EnumEntry, and the companion object extends Enum[Fruit]. findValues is a macro. When the code compiles, it collects every member of Fruit declared inside the companion object. Adding a case object adds it to values, with no list to forget. This is the actual output of the demo in the repo:
values : Vector(Apple, Banana, Cherry)
withName : Apple
withNameOption : None
withNameInsensitive: Some(Banana)
entryName : Cherry
indexOf : 1
Because it's still a sealed trait, exhaustive match checking works the same as in option 2. enumeratum doesn't replace the ADT approach. It's the ADT approach with the repetitive parts generated.
Side by side

My takeaway for now: Enumeration is fine for a throwaway script. A plain sealed trait is fine when no one ever needs to list the values or parse them from a string. In a backend where values arrive as strings in JSON, from the database or from Kafka, you need both. That's why my team uses enumeratum, and it now makes sense to me.
Run it yourself
git clone https://github.com/zakariahere/learning-enumeratum
cd learning-enumeratum
sbt "runMain lesson1.PlainScalaDemo" "runMain lesson1.EnumeratumDemo" Next up
Custom names and enums with fields: overriding entryName when the database or JSON spelling doesn't match the Scala name, values that carry data, and value enums like IntEnum and StringEnum.