Skip to main content
Version: 2.0.x

Validation

A Validation[E, A] is a data type that is either a Success with a value of type A or a Failure with one or more errors of type E.

You can think of a Validation as looking something like this:

import zio.NonEmptyChunk

sealed trait Validation[+E, +A]

object Validation {
case class Success[+A](value: A) extends Validation[Nothing, A]
case class Failure[+E](errors: NonEmptyChunk[E]) extends Validation[E, Nothing]
}

Thus, a Validation looks very much like an Either except that it can contain multiple errors instead of just one.

The distinguishing feature of Validation is that it can accumulate multiple errors. This makes it extremely convenient for problems like data validation where we want to accumulate all validation errors and report them back instead of failing immediately on the first validation error.

To see this, let's consider a simple example of validating a Person data type.

case class Person(name: String, age: Int)

The business requirements are that a valid person must have a name that is not empty and an age that is greater than zero.

We could try to model validating a Person using Either like this:

def validateName(name: String): Either[String, String] =
if (name.isEmpty) Left("Name was empty")
else Right(name)

def validateAge(age: Int): Either[String, Int] =
if (age <= 0) Left(s"Age $age was less than zero")
else Right(age)

def validatePerson(name: String, age: Int): Either[String, Person] =
for {
name <- validateName(name)
age <- validateAge(age)
} yield Person(name, age)

However, there is a problem with this. If we try to validate a Person with both an invalid name and an invalid age we will just get a single failure indicating that the name was empty.

This is because Either embodies "fail fast" semantics where if one part of a computation fails the subsequent parts of the computation will never be performed.

Normally this is what we want because there is no point in doing further work if a computation has already failed. This is similar to how in traditional imperative programs if an exception is thrown on one line then further lines will not be executed.

However, it is not what we want here.

We could imagine that the name and age were transmitted by a customer. They would probably not be happy if we told them the name was invalid, they corrected that and sent it again, and then we failed again because the age was invalid.

"Why didn't you tell me about all the problems up front?" we can see them saying. Validation does just that.

We can fix this example using Validation like so:

import zio.prelude.Validation

case class Person(name: String, age: Int)

def validateName(name: String): Validation[String, String] =
if (name.isEmpty) Validation.fail("Name was empty")
else Validation.succeed(name)

def validateAge(age: Int): Validation[String, Int] =
if (age <= 0) Validation.fail(s"Age $age was less than zero")
else Validation.succeed(age)

def validatePerson(name: String, age: Int): Validation[String, Person] =
Validation.validateWith(validateName(name), validateAge(age))(Person)

Here we are just using the basic operators on Validation of succeed and fail which construct a Validation that succeeds or fails with the specified value. We are then using the validateWith operator, which combines the results of multiple Validation values, accumulating all errors that occur.

Note that we are using the apply method of Person to combine the validated name and age into a Person. This is often a convenient pattern when we are modeling our data using case classes.

Now if we validate a person where both the name and the age are invalid we will get a failure containing both errors, giving us exactly what we need for our internal customer, so they can efficiently correct the problem and send us valid data.

Constructing Validation Values

Now that we understand what Validation is, let's look at how to construct Validation values.

From Existing Values

The simplest ways to construct Validation values are the succeed and fail operators we saw above:

object Validation {
def fail[E](error: E): Validation[E, Nothing] =
???
def succeed[A](value: A): Validation[Nothing, A] =
???
}

These just construct validation successes or failures with the specified value. We can use these as the building blocks for creating more complex validation logic as we saw above.

From Code That May Throw Exceptions

Another useful constructor of Validation values is the apply method on Validation:

object Validation {
def apply[A](a: => A): Validation[Throwable, A] =
???
}

The apply operator takes a by name argument and evaluates that argument, converting any non-fatal thrown exceptions into Validation failures. This is useful when working with code that might throw exceptions to safely import that code into a Validation value.

From Predicates

The fromPredicateWith constructor is useful for constructing Validation values from predicates. This is nice for cleaning up code that constructs a validation failure or success based on some condition like the example we wrote above:

object Validation {
def fromPredicateWith[E, A](error: => E)(value: A)(f: A => Boolean): Validation[E, A] =
???
}

The fromPredicateWith constructor constructs a Validation from a value, a predicate about that value, and an error to provide if the predicate is false.

For example, here is how we could simplify the example we wrote above using the fromPredicateWith operator:

def validateName(name: String): Validation[String, String] =
Validation.fromPredicateWith("Name was empty")(name)(_.nonEmpty)

def validateAge(age: Int): Validation[String, Int] =
Validation.fromPredicateWith(s"Age $age was less than zero")(age)(_ >= 0)

This is the same as the original example but lets us express our logic at a slightly higher level.

From Other Data Types

There are also operators for constructing Validation values from a variety of other data types in the Scala standard library such as Either, Option, and Try:

import scala.util.Try

object Validation {
def fromEither[E, A](value: Either[E, A]): Validation[E, A] =
???
def fromOption[A](value: Option[A]): Validation[Unit, A] =
???
def fromTry[A](value: => Try[A]): Validation[Throwable, A] =
???
}

Each of these operators just converts another data type that models potential failures into a Validation.

In the case of fromEither the Validation will fail with the same error type as the Either.

In the case of fromTry the error type is always Throwable, since a Try can fail with any Throwable. The fromTry operator will also catch any exception thrown in the construction of the Try value and convert it to a Validation failure.

A Validation constructed using fromOption has Unit for the error type since an Option contains no useful information about why a failure has occurred. We will see shortly when we learn about transforming Validation values how we can add additional information here about why a validation failure occurred.

Transforming Validation Values

Now that we know about creating Validation values, the next thing we need to know is how to transform them.

We can transform the value type of the Validation using the map operator:

trait Validation[+E, +A] {
def map[B](f: A => B): Validation[E, B]
}

We can transform the error type of the Validation using the mapError operator:

trait Validation[+E, +A] {
def mapError[E2](f: E => E2): Validation[E2, A]
}

This can be useful to transform the error type to our own internal data model. It can also be useful to add additional information about why a validation failure occurred, for example adding a descriptive error message if an optional value did not exist.

Combining Validation Values

We can also combine Validation values.

Accumulating Errors

The most common way to do this is with the validateWith operator we saw above:

object Validation {
def validateWith[E, A, B, C](
left: Validation[E, A],
right: Validation[E, B]
)(f: (A, B) => C): Validation[E, C] =
???
}

This will check each of the original Validation values and if they are both successes combine their values with the function f to produce a new successful Validation value. If one or more of the original Validation values are failures then the result will be a failure with all of the failures of both original Validation values.

There are variants of validateWith for combining up to twenty two different Validation values so even if you are working with large cases classes with many fields you can still use validateWith to combine them.

There is also a validate variant that combines multiple Validation values but does not take a combining function. In this case the returned Validation just contains a tuple with all the original values, and you can combine them yourself later using map.

If you have a whole collection of values you want to validate you can use the validateAll operator to validate them all at the same time.

Chaining Validations

The second way we might want to combine Validation values is by returning another Validation value based on the result of the first validation value. For example, we might first try to parse a string into an integer and then validate that the integer is within a reasonable range.

import zio.prelude.Validation

def validateInt(s: String): Validation[String, Int] =
Validation(s.toInt).mapError(_.getMessage)

def validateAge(age: Int): Validation[String, Int] =
Validation.fromPredicateWith(s"Age $age was less than zero")(age)(_ >= 0)

def validateAgeString(s: String): Validation[String, Int] =
for {
n <- validateInt(s)
_ <- validateAge(n)
} yield n

Here the second validation depended on the first one because if we could not parse the string into an integer then we would not even have an integer to validate. Just like with other data types we can express this with the flatMap operator and use a for comprehension to chain multiple validations together.

Note that when we chain validations like this we only do the second validation if the first one is successful so we will never see both errors here. If all we are doing is chaining then we don't actually need Validation and could just use Either.

However, frequently we want to do some validations with error accumulating semantics and chain others. Having flatMap on Validation makes it very easy for us to do this.

To see this, let's expand on our example of validating a Person data type from above. Now we will validate the person based on two String inputs using the logic we implemented above.

def validatePerson(name: String, age: String): Validation[String, Person] =
Validation.validateWith(validateName(name), validateAgeString(age))(Person)

Now we are combining accumulating errors and chaining validations.

Validating the age will proceed by validating that the age can be parsed into an Int and then that the age is not negative. This will return either a validated age or a validation error if either of these steps failed.

We are also validating that the name is not empty. The final result will be either a validated person or a validation error containing both any errors that occurred in validating the name and age.

By combining operators like validateWith and flatMap you can build complex validation logic that handles any of your problems in this or related domains.

Extracting Validation Values

The final thing we want to be able to do with Validation values is extract them to access the success value or errors and do something with them. This also includes transforming Validation values into other data types that we may be interested in.

The most basic way to do this is with the fold operator, which lets us provide functions specifying what we want to do with the success or failure.

trait Validation[+E, +A] {
def fold[B](failure: E => B, success: A => B): B
}

Using fold, we can get out the success value of the Validation by providing a function that says what we want to do if the Validation is a failure. For example, we might provide a default value or refail in the context of some other data type that can model failure such as ZIO.

There are a variety of helpful operators implemented in terms of fold for transforming Validation values into other data types we might be interested in. The most common of these are:

import zio.{IO, NonEmptyChunk}

trait Validation[+E, +A] {
def toEither: Either[NonEmptyChunk[E], A]
def toEitherWith[E2](f: NonEmptyChunk[E] => E2): Either[E2, A]
def toZIO: IO[E, A]
}

The toEither operator converts the Validation into an Either where the Left side of the other contains a NonEmptyChunk of all the errors that occurred if the Validation was a failure. The toEitherWith operator is similar to this but lets us provide our own function for converting the collection of errors that occurred into our own error type.

The toZIO operator is particularly useful because frequently we will do some data validation which we will model with Validation but then want to do some further work with the result that needs a ZIO effect such as writing it to a file or sending it over the wire.

With toZIO we can easily do our validation in the context of Validation and then "lift" it into the context of ZIO to compose with our other ZIO effects. One nice feature of this operator is that it will automatically preserve all validation errors that occurred in ZIO's Cause data type.