refined is a Scala library, started by Frank Thomas, that lets a type carry a constraint. An Int Refined Positive is an Int that has been proven positive, and a String Refined MatchesRegex["[A-Z]{3}"] is a string that has been checked against a pattern. Once a value has that type, every function that accepts it can stop re-validating, and the compiler rejects any attempt to pass an unchecked value.
The general idea of refinement types, and the Scala 3 library built around it, is covered in Scala Iron, in depth. This article is about refined itself, still common in Scala 2 code: how its parts fit, how values get in, its integrations, its costs and failure modes, and the move to Scala 3. The latest release at the time of writing is 0.11.4; the README documents Scala and Scala.js 2.12 and 2.13.
The three moving parts
refined is built from three pieces, and almost every error message or surprise makes sense once you can name them.
Refined[T, P]is a wrapper that holds a value of base type T and records, in its type, that predicate P held when it was built. It is a value class, so in simple cases the JVM sees only the underlying value. Every normal way to build one runs a check first. The infix formInt Refined Positiveis the same type asRefined[Int, Positive].- Predicates are plain types that describe a constraint and do nothing on their own:
Positive,NonEmpty,MaxSize[64],Interval.Closed[1, 65535],MatchesRegex["[a-z]+"],Uuid,IPv4and many more. Boolean combinatorsAnd,Or,Not,AllOfandAnyOfcompose them. Validate[T, P]is the type class that actually tests a T against a P and produces a description of the result. Each built-in predicate ships with instances for the types where it makes sense;Positiveworks for any numeric type,NonEmptyfor strings and collections.
Everything else in the library is a way to get a Validate instance to run, at compile time for literals or at run time for everything else. Both paths end in the same type.
Getting values in: literals, refineV and auto
Add the core module and, usually, one or two integrations. The version numbers must match across refined modules because they share internal type class definitions.
// build.sbt -- Scala 2.13
libraryDependencies ++= Seq(
"eu.timepit" %% "refined" % "0.11.4",
"eu.timepit" %% "refined-cats" % "0.11.4",
"eu.timepit" %% "refined-pureconfig" % "0.11.4",
"eu.timepit" %% "refined-scalacheck" % "0.11.4" % Test
)There are two entry points. For literals written in source code, the auto import provides an implicit conversion backed by a macro: the compiler evaluates the Validate instance on the literal while compiling, and either accepts the assignment or reports Predicate failed as a type error. refineMV[P](literal) does the same thing explicitly. For values that only exist at run time, refineV[P](value) returns Either[String, T Refined P], with the Left carrying a human-readable description of which part of the predicate failed.
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
import eu.timepit.refined.string._
// Literals are checked by a macro at compile time (Scala 2 only).
val port: Int Refined Interval.Closed[1, 65535] = 8080
// val bad: Int Refined Positive = -3 // does not compile: Predicate failed: (-3 > 0).
// Runtime values go through refineV and come back as an Either.
def parsePort(raw: Int): Either[String, Int Refined Interval.Closed[1, 65535]] =
refineV[Interval.Closed[1, 65535]](raw)
parsePort(70000) // Left("Right predicate of (!(70000 < 1) && !(70000 > 65535)) failed: ...")
// Literal types: native on 2.13, via shapeless Witness on 2.12.
type Currency = String Refined MatchesRegex["[A-Z]{3}"] // 2.13
// type Currency = String Refined MatchesRegex[W.`"[A-Z]{3}"`.T] // 2.12The auto import also brings autoUnwrap, which lets a refined value be used where its base type is expected, so port + 1 still works. That convenience goes in one direction only: widening from refined to raw is free, narrowing from raw to refined always costs a check.
On Scala 2.12, literal types such as "[A-Z]{3}" or 65535 cannot be written directly in type position, so older code uses shapeless Witness through the W alias. On 2.13, literal types are part of the language and MatchesRegex["[A-Z]{3}"] is written as is. When you find W.`...`.T noise in a codebase, it is usually safe to remove it once the project is on 2.13.
RefinedTypeOps: a companion per type
Repeating refineV[Interval.Closed[1, 65535]] at every call site leaks the predicate everywhere. The idiomatic fix is a type alias plus a companion object that extends RefinedTypeOps. The companion gives you from (returns Either), unsafeFrom (throws IllegalArgumentException), an unapply for pattern matching, and, with auto in scope on Scala 2, a checked apply for literals.
import eu.timepit.refined.api.{Refined, RefinedTypeOps}
import eu.timepit.refined.numeric.Positive
import eu.timepit.refined.string.MatchesRegex
object domain {
type AmountMinor = Long Refined Positive
object AmountMinor extends RefinedTypeOps[AmountMinor, Long]
type Currency = String Refined MatchesRegex["[A-Z]{3}"]
object Currency extends RefinedTypeOps[Currency, String]
final case class Payment(amount: AmountMinor, currency: Currency)
}
import domain._
AmountMinor.from(1999L) // Right(1999)
Currency.from("usd") // Left("Predicate failed: \"usd\".matches(\"[A-Z]{3}\").")
Currency.unsafeFrom("EUR") // throws IllegalArgumentException on bad input: tests and constants onlyThe library also ships ready-made aliases under eu.timepit.refined.types, such as PosInt, NonNegLong, NonEmptyString and PortNumber, each with its own companion. Prefer them to hand-written aliases for common constraints: they read the same across projects and the integrations already know them.
Keep unsafeFrom out of production paths; it exists for test fixtures and constants the macro cannot see.
Worked example: a payment request at the boundary
A payment API receives an amount in minor units (cents) and a currency code. The business rule is simple: the amount must be positive, and the currency must be three upper-case letters. The refinement belongs at the edge where untrusted data becomes domain data, and nowhere else.
A single Either stops at the first failure, which gives callers one error at a time. For API responses you usually want all of them, so the example converts each result to cats' EitherNel and combines them with parMapN, which accumulates errors into a non-empty list.
import cats.data.EitherNel
import cats.syntax.all._
final case class PaymentRequest(amount: Long, currency: String)
def toDomain(r: PaymentRequest): EitherNel[String, Payment] =
(
AmountMinor.from(r.amount).leftMap(e => s"amount: $e").toEitherNel,
Currency.from(r.currency.trim).leftMap(e => s"currency: $e").toEitherNel
).parMapN(Payment.apply)
toDomain(PaymentRequest(0L, "usd"))
// Left(NonEmptyList("amount: Predicate failed: (0 > 0).",
// "currency: Predicate failed: \"usd\".matches(...)."))Trace one request: {"amount": 0, "currency": "usd"}. AmountMinor.from(0L) runs the Positive Validate for Long and fails; Currency.from fails on the regex. parMapN collects both messages and the handler returns a 400. A valid request yields a Payment whose fields are already proven, so the ledger service downstream has no validation code at all.
refined's messages describe the predicate, not the business meaning. Treat them as developer diagnostics, attach the field name, and map them to user-facing text in one place.
Writing your own predicate
Any constraint you can express as a function from T to Boolean can become a predicate. Define an empty case class as the predicate type and put an implicit Validate in its companion so the compiler finds it without an import. Validate.fromPredicate takes the check, a function that describes the expression for error messages, and an instance of the predicate.
import eu.timepit.refined.api.{Refined, Validate}
// A predicate is just a type; the evidence that checks it is a Validate instance.
final case class LuhnValid()
object LuhnValid {
private def luhn(s: String): Boolean =
s.nonEmpty && s.forall(_.isDigit) && {
val sum = s.reverse.map(_.asDigit).zipWithIndex.map {
case (d, i) if i % 2 == 1 => if (d * 2 > 9) d * 2 - 9 else d * 2
case (d, _) => d
}.sum
sum % 10 == 0
}
implicit val luhnValidate: Validate.Plain[String, LuhnValid] =
Validate.fromPredicate(luhn, s => s"luhn($s)", LuhnValid())
}
type CardNumber = String Refined LuhnValidCustom predicates compose with the built-in ones, so String Refined And[MinSize[12], LuhnValid] works. Two cautions apply. The macro path evaluates the Validate instance inside the compiler, so a predicate defined in the same compilation unit cannot be used for literal checks; keep custom predicates in a separate module if you want compile-time literals. And keep the check pure and fast: it runs on every decode, every config load and every generated test value.
Integrations: JSON, config, databases and tests
refined earns most of its value at system boundaries, and the ecosystem has modules for the common ones. Each integration derives its codec from the Validate instance, so a custom predicate works everywhere as soon as its instance exists.
// circe: decoders and encoders for any T Refined P that has a Validate
import io.circe.generic.auto._
import io.circe.parser.decode
import io.circe.refined._
decode[Payment]("""{"amount":0,"currency":"EUR"}""")
// Left(DecodingFailure("Predicate failed: (0 > 0).", ...))
// pureconfig: bad config fails at startup, not at first use
import eu.timepit.refined.pureconfig._
import pureconfig._
import pureconfig.generic.auto._
final case class HttpConfig(port: Int Refined Interval.Closed[1, 65535], host: String Refined NonEmpty)
val cfg = ConfigSource.default.at("http").loadOrThrow[HttpConfig]
// doobie: read and write refined columns; a bad row fails the query, not later code
import doobie.refined.implicits._
// scalacheck: generators that only produce valid values
import eu.timepit.refined.scalacheck.all._| Boundary | Module | What happens on bad input |
|---|---|---|
| JSON with circe | circe-refined (published by circe) | Decoding fails with a DecodingFailure naming the predicate; see circe, in depth |
| Configuration | refined-pureconfig | Loading fails at startup with the key path and message |
| SQL with doobie | doobie-refined | Reading the row fails the query; see doobie, in depth |
| Property tests | refined-scalacheck | Arbitrary instances generate only valid values |
Configuration deserves emphasis: a port of 0 in a config file should stop the service at boot, not surface as a connection error hours later.
In tests, ask for Arbitrary[AmountMinor] instead of hand-writing generators; values satisfy the type by construction. In http4s services, the same circe decoders drive request parsing.
What refined costs
Run-time cost is small but not zero. Refined is a value class, so a PosInt field is stored as an int. Value classes box whenever they are used generically, though: inside List[PosInt], Option[PosInt] or any type parameter, each element becomes a heap object. In hot numerical loops over large collections, unwrap to the base type before the loop.
Compile-time cost is real on Scala 2. Literal checks run a macro per use, and implicit search for Validate instances on large predicate compositions can slow typing noticeably. Codebases that refine hundreds of literals in test fixtures sometimes see measurable compile-time increases; moving such fixtures to unsafeFrom in a helper, or to scalacheck generators, is the usual fix. The macro machinery also leans on implicit resolution, so how Scala implicits resolve is useful background when an instance is not found.
Failure modes and gotchas
- Refining too deep. If internal functions call
refineVon values that are already refined, the types are not doing their job. Refine at the boundary, pass refined types inward, and treat an innerrefineVas a design smell. - Arithmetic loses the proof. Adding two
PosIntvalues with autoUnwrap yields a plain Int, and it can overflow to a negative number. refined does not track constraints through arithmetic; re-refine the result if you need the guarantee. - Regex drift.
MatchesRegexuses Java'sString.matches, which anchors the whole string. Patterns copied from other languages that expect partial matching will behave differently. - Mismatched module versions. Mixing refined 0.9.x in one dependency with 0.11.x in another produces linkage errors or missing instances. Pin one version in your build and check the evicted-dependency report.
- Serialisation formats that bypass codecs. Java serialisation, reflection-based mappers and some Spark encoders construct objects without calling your decoders, which lets unchecked values into refined fields. At those boundaries, decode into raw types and refine explicitly.
Refined.unsafeApply. It is public and skips validation entirely. Ban it in main sources as you wouldunsafeFrom.- Leaking messages. Predicate descriptions echo the input value. For secrets or personal data, such as card numbers, replace the message before logging it.
Moving to Scala 3
The refined core is published for Scala 3, but the compile-time part is not. The macros behind auto and refineMV use the Scala 2 macro API's c.eval, which Scala 3 does not provide, so on Scala 3 you keep refineV, RefinedTypeOps.from and runtime validation, and lose literal checks. Code that relied on val n: PosInt = 5 must switch to unsafeFrom or another construction. Third-party compatibility shims exist; evaluate them like any other dependency.
For new Scala 3 code, the usual recommendation is Iron, which uses Scala 3 inline and opaque types to check literals at compile time with no wrapper at run time; see Scala Iron, in depth and Scala 3 opaque types. A practical migration path is to cross-build on 2.13 and 3 with refined for runtime refinement only, move each module's domain types to Iron once it is Scala 3 only, and keep the codec boundary the same so that clients see no change in behaviour.
What to do next
- Pick one boundary in a Scala 2 service, such as a config class or one API request type, and replace raw fields with refined aliases from
eu.timepit.refined.types. - Add
refined-pureconfigand confirm that a bad config value stops the service at startup with a readable message. - Give each custom domain type an alias plus a
RefinedTypeOpscompanion, and search main sources forunsafeFrom. - Accumulate validation errors with
EitherNelandparMapN, and map predicate messages to user-facing text in one place. - Switch property tests to
refined-scalacheckgenerators for refined fields. - If you are heading to Scala 3, list every literal refinement in the codebase now; each one needs a new construction or a move to Iron.