Case classes and pattern matching
Model data with case classes, then take it apart with match — the two features that most Scala code is built from.
Case classes
A case class is an ordinary class plus generated code: a factory in a companion object, accessors, structural equality, copy, a readable toString and an unapply method, which is what makes pattern matching work.
case class Address(street: String, city: String)
case class Person(name: String, age: Int, address: Option[Address])
val ada = Person("Ada", 36, Some(Address("1 Main St", "London")))
println(ada.name) // accessor
println(ada) // Person(Ada,36,Some(Address(1 Main St,London)))
println(ada == Person("Ada", 36, None)) // false: equality compares the fields
val nextBirthday = ada.copy(age = ada.age + 1)
val moved = nextBirthday.copy(address = ada.address.map(a => a.copy(city = "Leeds")))
// a sealed hierarchy: the compiler knows every subtype
sealed trait Shape
case class Circle(radius: Double) extends Shape
case object Unit extends Shape // no fields, so an object and not a class| Generated member | What it gives you |
|---|---|
The factory Person.apply | Construction without the new keyword |
equals and hashCode | Value equality, so it is safe as a map key |
copy | A new instance with selected fields changed |
unapply | Field extraction inside a pattern match |
toString | A readable representation for logs and debugging |
productIterator | Iteration over the fields, useful for generic code |
Pattern matching
def describe(shape: Shape): String = shape match {
case Circle(r) if r > 10 => "large circle"
case Circle(r) => s"circle of radius $r"
case Unit => "unit"
// no default branch: with a sealed hierarchy the compiler warns if a case is missing
}
text match {
case "" => "empty"
case s if s.length > 100 => "long"
case s => s"$s (${s.length} chars)"
}
// destructuring tuples and sequences
val person = (1, "Ada", true)
val (id, who, active) = person
val names = List("Ada", "Grace", "Alan")
names match {
case Nil => "nobody"
case one :: Nil => s"only $one"
case first :: rest => s"$first and ${rest.size} more"
}
// matching on a type, with a binding
def size(value: Any): String = value match {
case s: String => s"string of ${s.length}"
case i: Int => s"int $i"
case _ => "unknown"
}⚠️
A non-exhaustive
match over an unsealed type compiles with only a warning and throws MatchError at run time. Model alternatives as a sealed trait so the compiler can prove that every case is covered and refuse to build when one is added and forgotten.Option, Either and Try
def findUser(id: Long): Option[User] = ???
// handle both branches; never call get
val label = findUser(1) match {
case Some(u) => u.name
case None => "guest"
}
val upper = findUser(1).map(_.name.toUpperCase).getOrElse("GUEST")
val city = findUser(1).flatMap(_.address).map(_.city) // Option[String]
// Either carries a reason for the failure
def parseAge(text: String): Either[String, Int] =
text.toIntOption.toRight(s"not a number: $text")
val age = parseAge("36").map(_ + 1) // Right(37)
// Try turns a thrown exception into a value
import scala.util.{ Try, Success, Failure }
Try("abc".toInt) match {
case Success(n) => s"parsed $n"
case Failure(e) => s"failed: ${e.getMessage}"
}Option[A]replaces null: eitherSome(a)orNone, and the compiler will not let you forget the empty case.Either[E, A]keeps the failure value, which is usually a typed error rather than an exception message.Try[A]wraps code that throws, so it is the bridge to Java libraries.- Return these types from public methods rather than throwing, so callers cannot overlook the failure case.
FAQ
Why does pattern matching work on a case class?
The compiler generates a companion
unapply method that takes an instance apart into its fields. A plain class can do the same by declaring an unapply or by extending Product.When should I use a case object rather than a case class?
When the alternative carries no data, such as
None or a Pending state. A case object is a singleton with the same equality and pattern-matching support, and it allocates nothing.Related
Syntax and immutability Collections and functional style
Last refreshed 2026-09-18.