Scala - Enumerations

Scala 2's `scala.Enumeration` exists primarily for Java interoperability. It uses runtime reflection and lacks compile-time type safety.

Key Insights

  • Scala provides three enumeration approaches: legacy scala.Enumeration, Scala 3’s native enum, and sealed trait hierarchies, each with distinct trade-offs for type safety and pattern matching
  • Scala 3 enums compile to sealed classes with singleton instances, offering algebraic data type (ADT) capabilities that far exceed simple value lists
  • For production systems, sealed traits provide maximum flexibility and type safety in Scala 2, while Scala 3 enums deliver cleaner syntax with equivalent power

Legacy Scala Enumeration

Scala 2’s scala.Enumeration exists primarily for Java interoperability. It uses runtime reflection and lacks compile-time type safety.

object Color extends Enumeration {
  type Color = Value
  val Red, Green, Blue = Value
}

import Color._

def describeColor(color: Color): String = color match {
  case Red   => "warm"
  case Green => "natural"
  case Blue  => "cool"
  // Warning: match may not be exhaustive
}

// Usage
val myColor: Color = Color.Red
println(Color.values) // Set(Red, Green, Blue)
println(myColor.id)   // 0

The fundamental problem: the compiler cannot verify pattern match exhaustiveness. Add a new color, and existing matches won’t generate warnings.

object Status extends Enumeration {
  val Pending, Active, Completed, Failed = Value
  
  // Custom values and names
  val Archived = Value(10, "ARCHIVED")
  
  def fromString(s: String): Option[Value] = 
    values.find(_.toString == s)
}

// Type erasure issues
def process(status: Status.Value): Unit = {
  // status.type is just Enumeration.Value at runtime
}

Avoid scala.Enumeration for new code. It’s a pre-2.10 design that predates modern Scala idioms.

Sealed Trait Hierarchies (Scala 2 & 3)

Sealed traits provide compile-time exhaustiveness checking and full type system integration.

sealed trait HttpMethod
case object GET extends HttpMethod
case object POST extends HttpMethod
case object PUT extends HttpMethod
case object DELETE extends HttpMethod
case object PATCH extends HttpMethod

def requiresBody(method: HttpMethod): Boolean = method match {
  case POST | PUT | PATCH => true
  case GET | DELETE       => false
  // Compiler error if we miss a case
}

// Type-safe pattern matching
def routeRequest(method: HttpMethod, path: String): Unit = {
  method match {
    case GET    => handleGet(path)
    case POST   => handlePost(path)
    case PUT    => handlePut(path)
    case DELETE => handleDelete(path)
    case PATCH  => handlePatch(path)
  }
}

Add parameters for richer ADTs:

sealed trait Result[+T]
case class Success[T](value: T) extends Result[T]
case class Failure(error: String, code: Int) extends Result[Nothing]
case object Pending extends Result[Nothing]

def processResult[T](result: Result[T]): Option[T] = result match {
  case Success(value) => Some(value)
  case Failure(err, code) => 
    println(s"Error $code: $err")
    None
  case Pending => None
}

// Usage with type inference
val result: Result[Int] = Success(42)
processResult(result) // Some(42)

Sealed traits with case objects provide enumeration-like behavior while maintaining full type safety:

sealed trait LogLevel {
  def priority: Int
}

case object Debug extends LogLevel { val priority = 0 }
case object Info extends LogLevel  { val priority = 1 }
case object Warn extends LogLevel  { val priority = 2 }
case object Error extends LogLevel { val priority = 3 }

object LogLevel {
  val all: Set[LogLevel] = Set(Debug, Info, Warn, Error)
  
  def fromString(s: String): Option[LogLevel] = s.toUpperCase match {
    case "DEBUG" => Some(Debug)
    case "INFO"  => Some(Info)
    case "WARN"  => Some(Warn)
    case "ERROR" => Some(Error)
    case _       => None
  }
}

def shouldLog(current: LogLevel, minimum: LogLevel): Boolean =
  current.priority >= minimum.priority

Scala 3 Native Enums

Scala 3 introduces first-class enum support with clean syntax and ADT capabilities.

enum Direction:
  case North, South, East, West

def opposite(dir: Direction): Direction = dir match
  case Direction.North => Direction.South
  case Direction.South => Direction.North
  case Direction.East  => Direction.West
  case Direction.West  => Direction.East

// Enums with parameters
enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

println(Color.Red.rgb) // 16711680

Enums compile to sealed classes with case objects, providing the same guarantees as manual sealed trait hierarchies:

enum HttpStatus(val code: Int, val message: String):
  case Ok           extends HttpStatus(200, "OK")
  case Created      extends HttpStatus(201, "Created")
  case BadRequest   extends HttpStatus(400, "Bad Request")
  case Unauthorized extends HttpStatus(401, "Unauthorized")
  case NotFound     extends HttpStatus(404, "Not Found")
  case ServerError  extends HttpStatus(500, "Internal Server Error")

def isSuccess(status: HttpStatus): Boolean = 
  status.code >= 200 && status.code < 300

// Pattern matching with guards
def handleResponse(status: HttpStatus): String = status match
  case s if s.code < 300 => s"Success: ${s.message}"
  case s if s.code < 500 => s"Client error: ${s.message}"
  case s                 => s"Server error: ${s.message}"

Enums support methods and companion object utilities:

enum Operation:
  case Add, Subtract, Multiply, Divide
  
  def symbol: String = this match
    case Add      => "+"
    case Subtract => "-"
    case Multiply => "*"
    case Divide   => "/"
  
  def apply(a: Int, b: Int): Int = this match
    case Add      => a + b
    case Subtract => a - b
    case Multiply => a * b
    case Divide   => a / b

object Operation:
  def fromSymbol(s: String): Option[Operation] = s match
    case "+" => Some(Add)
    case "-" => Some(Subtract)
    case "*" => Some(Multiply)
    case "/" => Some(Divide)
    case _   => None

// Usage
val op = Operation.Multiply
println(op.symbol)      // *
println(op(6, 7))       // 42

ADTs with Enums

Scala 3 enums excel at modeling algebraic data types:

enum Json:
  case JNull
  case JBoolean(value: Boolean)
  case JNumber(value: Double)
  case JString(value: String)
  case JArray(elements: List[Json])
  case JObject(fields: Map[String, Json])

def stringify(json: Json): String = json match
  case Json.JNull           => "null"
  case Json.JBoolean(b)     => b.toString
  case Json.JNumber(n)      => n.toString
  case Json.JString(s)      => s"\"$s\""
  case Json.JArray(elems)   => elems.map(stringify).mkString("[", ",", "]")
  case Json.JObject(fields) => 
    fields.map((k, v) => s"\"$k\":${stringify(v)}").mkString("{", ",", "}")

// Example usage
val data = Json.JObject(Map(
  "name" -> Json.JString("Alice"),
  "age" -> Json.JNumber(30),
  "active" -> Json.JBoolean(true)
))

println(stringify(data)) 
// {"name":"Alice","age":30.0,"active":true}

Enums integrate with generics and variance:

enum Validation[+E, +A]:
  case Valid(value: A) extends Validation[Nothing, A]
  case Invalid(errors: List[E]) extends Validation[E, Nothing]
  
  def map[B](f: A => B): Validation[E, B] = this match
    case Valid(a) => Valid(f(a))
    case Invalid(e) => Invalid(e)
  
  def flatMap[E2 >: E, B](f: A => Validation[E2, B]): Validation[E2, B] = 
    this match
      case Valid(a) => f(a)
      case Invalid(e) => Invalid(e)

// Validation example
def validateAge(age: Int): Validation[String, Int] =
  if age >= 0 && age <= 150 then Validation.Valid(age)
  else Validation.Invalid(List("Age must be between 0 and 150"))

def validateName(name: String): Validation[String, String] =
  if name.nonEmpty then Validation.Valid(name)
  else Validation.Invalid(List("Name cannot be empty"))

Serialization and Java Interop

For JSON serialization with libraries like Circe or Play JSON, sealed traits and enums work seamlessly:

// Scala 3 enum with Circe
import io.circe.{Encoder, Decoder}
import io.circe.generic.semiauto._

enum Priority:
  case Low, Medium, High, Critical

object Priority:
  given Encoder[Priority] = Encoder.encodeString.contramap(_.toString)
  given Decoder[Priority] = Decoder.decodeString.emap { str =>
    Priority.values.find(_.toString == str)
      .toRight(s"Invalid priority: $str")
  }

For Java interoperability, use explicit ordinal methods:

enum Status:
  case Pending, Active, Completed
  
  def ordinal: Int = this.ordinal

object Status:
  def fromOrdinal(i: Int): Option[Status] = 
    Status.values.lift(i)

// Java-friendly API
class StatusAPI:
  def getStatusCode(status: Status): Int = status.ordinal
  def getStatusFromCode(code: Int): Status = 
    Status.fromOrdinal(code).getOrElse(Status.Pending)

Practical Recommendations

Use Scala 3 enums for new Scala 3 projects—they provide clean syntax with full type safety. Use sealed trait hierarchies in Scala 2 or when you need maximum flexibility. Avoid scala.Enumeration unless maintaining legacy code or requiring Java enum compatibility.

For domain modeling, prefer enums with parameters over simple value lists. The type system catches errors at compile time, and pattern matching becomes self-documenting code.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.