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 memberWhat it gives you
The factory Person.applyConstruction without the new keyword
equals and hashCodeValue equality, so it is safe as a map key
copyA new instance with selected fields changed
unapplyField extraction inside a pattern match
toStringA readable representation for logs and debugging
productIteratorIteration 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: either Some(a) or None, 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.

Syntax and immutability Collections and functional style

Last refreshed 2026-09-18.