Validation in Scala 3 with Cats
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.
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 emptyinitialAmount
must be positiveuserId
must be positivecreatedAt
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 method
from 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 3
extension 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.