Validation in Scala 3 with Cats

Sergio Cano
4 min readMay 23, 2022

--

In this post we are going to see how to validate entities using Cats in Scala 3. Also, it is a great opportunity to see some Scala 3 syntax and mechanisms.

Photo by eberhard 🖐 grossgasteiger inUnsplash

A quick intro to Validation

Imagine that we have an API for managing accounts. For this example, we are going to use the following entity:

An account to be valid needs to satisfy the following rules:

  • name must not be empty
  • initialAmount must be positive
  • userId must be positive
  • createdAt must be less than current time

In order to properly validate those fields, our first approach could be something based on Either, which is a datatype that allow us to represent a computation that can be either an error or a success. So, we create the following methods:

Either is a Monad, so we can evaluate all this rules using a for-comprehension :

The problem with that approach is that Either short-circuits the operation at the first error ( Left value) it gets. So, for example, if we have the following:

Just the first invalid result is informed. If the user fixes this error, she will have to retry over and over again until all the remaining fields were ok, which is a really bad experience for an API user. In this case, it would be great to have a way to express all the invalid reasons at once. Let’s try to find another datatype that can help us to reach that goal.

Before moving to the next section, let’s do a little refactor to typify our validations.

Validated

Validated is a datatype provided by Cats that is similar to Either because it can take two possible values: Valid or Invalid:

We can refactor our validations to use Validated instead.

The problem with this approach is that we cannot use our former validate method (which is based on for-comprehensions). It is because Validated is not a Monad, so it doesn’t have flatMap. Instead, Validated is an Applicative Functor. So, we need to apply refactors to this method, but before doing that, let’s massage our data structures a little bit.

Using Applicative

for-comprehensions are fail-fast, which is not suitable for our use case. Also, we need to find a way to accumulate our errors. In this case, we can try to use another structure to express the invalid side of our Validated:

NonEmptyList is a data structure that guarantees holding at least one element, which is what we are looking for in case of having validation errors.

ValidationResult[T] is a convenient alias for re-utilize this type definition.

Also in Cats, the data type Validated[NonEmptyList[E], A] has an alias: ValidatedNel[E, A] . In our case, E is represented by AccountValidation which is the type we choose to represent our validation errors. So, we can rewrite our definition:

Having all this set up, we can refactor our validation methods:

validNel and invalidNel are syntax methods provided by the Validated object ( import cats.data.Validated._). Those methods allow us to lift our values to ValidatedNel context.

Now, we can rewrite our validate method, but usingApplicative syntax this time, more specifically, the mapN method.

mapN allow us to apply all the validations, returning the success values as a tuple, or the accumulated validation error list.

Let’s see how it works:

So, we achieved our goal of having an accumulated list of valition errors in case of failure.

Some words about Applicative and mapN

mapN is a syntax methodfrom Applicative, which extends Semigroupal under the hood. More specifically, the method product from Semigroupal. This method allow us to combine two independent effects F[A] and F[B] into F[(A, B)].

In case of Validated, the product method accumulate validation errors if founds any. Otherwise, it returns the product ( Valid[(A, B, C, …)])).

Semigroupal and Applicative typeclasses are useful in cases like this, when we require more flexibility in the way we execute our effects (in contrast with Monad, which enforces sequential execution with fail-fast semantics). In practice, you are likely to work with Applicative syntax instead of Semigroupal directly. You can find more info about Applicative in the Cats documentation. Also, the book Essential Effects gives a great intro to this topic and its importance when applying to concurrent effect evaluation.

An improvement

We can enrich our Account case class adding validate as an extension method. For doing that, we can take advantage of Scala 3extension methods:

This can allow us to have a cleaner syntax when performing validations in other places of our code:

Testing our validations

Following, there is our test suite. It is a very regular one using just plain Scalatest:

What the F[_]???

During this tutorial we were talking about data validation using Validated, which is an specific data structure. However, you can also abstract over the concept of validation and not being tied to any particular implementation. This last approach is more evident when your code is strongly based on polymorphic effects. In cases like that, you can check ApplicativeError and MonadError.

Conclusions

In this post we have seen how we can use Validated datatype from Cats to validate our domain objects and how we can use it in Scala 3. The code is available on github.

--

--

Sergio Cano

A software developer that loves solving problems and JVM languages (specially Scala and Java)