scalaintermediate
Error Accumulation with Validated
Accumulate multiple validation errors instead of failing fast using Cats Validated and custom validators.
scalaPress ⌘/Ctrl + Shift + C to copy
import cats.data.*
import cats.syntax.all.*
// Domain types
case class RegistrationForm(
username: String,
email: String,
password: String,
age: String
)
case class ValidatedUser(
username: String,
email: String,
passwordHash: String,
age: Int
)
sealed trait ValidationError:
def message: String
case class EmptyField(field: String) extends ValidationError:
def message = s"$field cannot be empty"
case class TooShort(field: String, min: Int, actual: Int) extends ValidationError:
def message = s"$field must be at least $min chars (got $actual)"
case class InvalidFormat(field: String, hint: String) extends ValidationError:
def message = s"$field: $hint"
case class OutOfRange(field: String, min: Int, max: Int) extends ValidationError:
def message = s"$field must be between $min and $max"
type VResult[A] = ValidatedNel[ValidationError, A]
// Individual validators
def validateUsername(s: String): VResult[String] =
if s.isEmpty then EmptyField("username").invalidNel
else if s.length < 3 then TooShort("username", 3, s.length).invalidNel
else if !s.matches("^[a-zA-Z0-9_]+$") then
InvalidFormat("username", "alphanumeric and underscore only").invalidNel
else s.validNel
def validateEmail(s: String): VResult[String] =
if s.isEmpty then EmptyField("email").invalidNel
else if !s.matches(".+@.+\\..+") then
InvalidFormat("email", "must be a valid email").invalidNel
else s.validNel
def validatePassword(s: String): VResult[String] =
val errors = List(
Option.when(s.length < 8)(TooShort("password", 8, s.length)),
Option.when(!s.exists(_.isUpper))(InvalidFormat("password", "must contain uppercase")),
Option.when(!s.exists(_.isDigit))(InvalidFormat("password", "must contain a digit"))
).flatten
NonEmptyList.fromList(errors) match
case Some(nel) => Validated.invalid(nel)
case None => s.hashCode.toString.validNel // fake hash
def validateAge(s: String): VResult[Int] =
s.toIntOption match
case None => InvalidFormat("age", "must be a number").invalidNel
case Some(a) if a < 13 || a > 120 => OutOfRange("age", 13, 120).invalidNel
case Some(a) => a.validNel
def validate(form: RegistrationForm): VResult[ValidatedUser] =
(
validateUsername(form.username),
validateEmail(form.email),
validatePassword(form.password),
validateAge(form.age)
).mapN(ValidatedUser.apply)
@main def run(): Unit =
// Valid form
val good = RegistrationForm("alice_42", "alice@test.com", "Secret123", "25")
println(s"Good: ${validate(good)}")
// All fields invalid — accumulates ALL errors
val bad = RegistrationForm("", "not-email", "short", "abc")
validate(bad) match
case Validated.Invalid(errors) =>
println("Errors:")
errors.toList.foreach(e => println(s" - ${e.message}"))
case Validated.Valid(_) => println("Valid")
// Some fields invalid
val partial = RegistrationForm("ab", "alice@test.com", "no", "10")
validate(partial) match
case Validated.Invalid(errors) =>
println(s"Partial errors (${errors.size}):")
errors.toList.foreach(e => println(s" - ${e.message}"))
case _ => ()Use Cases
- Form validation with all errors
- Input validation pipelines
- API request validation
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
scalaadvanced
Cats Monad Transformers
Use Cats monad transformers: EitherT, OptionT, and StateT for composable effect stacks.
Best for: Composable error handling stacks
#scala#cats
scalaadvanced
Effect Composition Patterns
Compose effects with traverse, sequence, parTraverse, and error accumulation patterns.
Best for: Parallel API calls
#scala#effects
scalaintermediate
Refined and Constrained Types
Create constrained types with validation: non-empty strings, bounded numbers, and validated domain types.
Best for: Domain type validation
#scala#refined
scalabeginner
Scala Hello World Application
Create a basic Scala application with main method, string interpolation, and val/var basics.
Best for: Getting started with Scala
#scala#basics