EXPEDIA GROUP TECHNOLOGY — SOFTWARE
Applicative and Validated Scala Cats: Nested Form Validation
Usage of the Cats library Applicative and Validated type classes to perform multilevel form validation. So much fun guaranteed…
A quick search on Google will return several posts on form validation using the Scala Cats library. It’s difficult though to find a good example of nested form validation with Cats. In this post we show how to compose Cats Applicative and Validated functors to ensure all elements of a form will be validated.
The Applicative Cats type class is well suited to form validation because of its parallel computation nature. While in a monad everything is computed sequentially, in an Applicative functor the computation is not restricted to be executed in any particular order. It is executed in parallel. While in a monad if during the execution of the monad it fails, the execution is stopped, in an applicative due to its nature, the computation continues.
With Applicatives we are able to return all the errors that one form has, while if we would use a monad to try the same, we could just return the first error. That’s why Applicative functors are so useful for form validation. The validation of every field can be done in parallel and at the end the result of the validation can be joined and merged at the end of the process.
The goal of this post is to show how to implement a nested form validation using Cats. The majority of the examples you can find online are just a one-level form, like for instance an Address
:
case class Address(street: String, number: String, postalCode: String, county: Option[String], country: String)
But in the real world forms are quite complex, and normally the structures contain three or four nested objects. For instance:
case class Property(listing: Listing)
case class Listing(id: Option[String], contact: Contact, address: Address, location: GeoLocation)
case class Contact(phone: String, formattedPhone: String)
case class Address(address: String, postalCode: String, countryCode: String, city: String, state: Option[String], country: String)
case class GeoLocation(lat: String, lng: String)
In the previous code it has been modeled a hotel property listing. Obviously we could have modeled a form validator based on one big validator that validates all the different values of the nested elements. But the solution is not quite clean and it is quite coupled. For instance the address case class could be used in other parts of our code as part of another class.
Let’s review what it is exactly the syntax of the Applicative functor:
trait Applicative[F[_]] extends Apply[F]{
def pure[A](x: A): F[A]
override def map[A, B](fa: F[A])(f: A => B): F[B] = ap(pure(f))(fa)
}
It contains 2 functions:
- Pure containerize an object of type
A
intoF[A]
. If our applicative is applied over the monad List, it will add the element to the List. The same applies to Option or Future. - Map: as applicative is a functor it should have the map operation. We can see that to implement the map operation it has been done in terms of pure and
ap
. The operationap
is the most important operation of Applicative. It is implemented in the class Apply that this class is mixing.
This is the syntax of the Apply trait:
trait Apply[F[_]] extends Functor[F] {
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
}
As we can see it has two curried parameters. The first contains a function inside of our container F[A =>B]
and the second is the containerized first element of the function. First time you read this syntax it could seem a cryptic, but you will see the usage more clearly once we start with our example.
Let’s do it!
We want to create a composite validator using Cats for the following form:
case class Property(listing: Listing)
case class Listing(id: Option[String], contact: Contact, address: Address, location: GeoLocation)
case class Contact(phone: String, formattedPhone: String)
case class Address(address: String, postalCode: String, countryCode: String, city: String, state: Option[String], country: String)
case class GeoLocation(lat: String, lng: String)
We can start with the implementation of the AddressValidator
:
def validateAddress(address: String): F[String] = address.pure[F]def validatePostalCode(postalCode: String): F[String] = postalCode.pure[F]def validateCountryCode(countryCode: String): F[String] =
if (countryCode.size == 2) countryCode.pure[F]
else
A.raiseError(mkError(AddressNonCorrectCountryCodeError))def validateState(address: Address): F[Option[String]] = {
if (address.countryCode == "US") address.state match {
case None => A.raiseError(mkError(AddressShouldContainStateError))
case Some(_) => address.state.pure[F]
}
else {
address.state match {
case None => address.state.pure[F]
case Some(_) => A.raiseError(mkError(AddressShouldNotContainStateError))
}
}
}
def validateCity(city: String): F[String] =
if (city.isEmpty)
A.raiseError(mkError(AddressCityIsEmptyError))
else
city.pure[F]
def validateCountry(country: String): F[String] =
if (country.isEmpty)
A.raiseError(mkError(AddressCountryIsEmptyError))
else
country.pure[F]
def validate(address: Address): F[Address] = {
(Address.apply _).curried.pure[F].
ap(validateAddress(address.address)).
ap(validatePostalCode(address.postalCode)).
ap(validateCountryCode(address.countryCode)).
ap(validateCity(address.city)).
ap(validateState(address)).
ap(validateCountry(address.country))
}
Considerations of the previous code:
- validate
function: it receives an address as a parameter and it returns a monad of address, in this case we do not know the kind of monad until this class is instantiated.
-(Address.apply _).curried.pure[F]
: we are building an address as a curried nested function of parameters. What (Address.apply _).curried
does? It converts a constructor case class Address
into a nested function:
case class Address(address: String, postalCode: String, countryCode: String, city: String, state: Option[String], country: String)
to
String => String => String =>String => Option[String] => String => Address
Magic? No, Scala.
- Continuation of code comments: so, we have a nested function and we applied the pure
function from Applicative, so we have a:
F[String => String => String =>String => Option[String] => String => Address]
- Looks like the first parameter of the ap
function from Aplicative class, isn’t it?
- The lines after the curried constructor: we are able to apply the method ap to the function F[String => String …]
and construct our F[Address]
.
- You can realize that when we construct our F[Address]
we are calling a method validateCountry, validatePostalCode… These functions check the validity of each attribute and return a F[]
.
- In all the validation methods, you can see that to construct a valid value we just need to call the F[A].pure
to containerize the value.
- In case that there is an error, we can raise it using A.raiseError(mkError)
.
In the AddressValidator
we have seen the root of our validators, but what about the validator that is a composition of other validators. We need to make use of another class provided by Cats called Validated. Let’s understand Validated first:
sealed abstract class Validated[+E, +A] extends Product with Serializable {
def fold[B](fe: E => B, fa: A => B): B =
this match {
case Invalid(e) => fe(e)
case Valid(a) => fa(a)
}
def isValid: Boolean = fold(_ => false, _ => true)
def isInvalid: Boolean = fold(_ => true, _ => false)
Validated is not a monad. If you try to execute the flatMap
operation, you can see that doesn’t exist. Validated is a type class that computes if an object is valid or invalid. It can be used in conjunction with the Applicative type class to allow accumulation of errors. In other cases we may want to have a monadic validation, and fail with the first error. In that case we could use the Either monad.
Validated has 2 type parameters E
and A
. The E
type correspond to the kind of error that is returned in case of error. The A
type parameter correspond to the return type in case of a valid response. Similar to the Either
type parameters.
To help us, Cats provides an extension type of the main Validated type, that we are going to use in our example:
type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]
The cats.data.ValidatedNel
models the error accumulation use case, that it is exactly what we need.
We can see part of the implementation of the ListingValidator
.
def validateId(idOpt: Option[String], emptyID: Boolean): Validated[PropertyValidationError, Option[String]] = {
emptyID match {
case true => idOpt match {
case None => Valid(idOpt)
case Some(_) => Invalid(IdNonEmptyError)
}
case false => idOpt match {
case None => Invalid(IdEmptyError)
case Some(id) => Try(UUID.fromString(id)).isSuccess match {
case true => Valid(idOpt)
case false => Invalid(NonUUIDFormatError)
}
}
}
}def validateGeoLocation(location: GeoLocation): Validated[PropertyValidationError, GeoLocation] = {
val tryLat = Try(location.lat.toDouble)
val tryLong = Try(location.lng.toDouble)
if (tryLat.isSuccess && tryLong.isSuccess)
Valid(location)
else Invalid(GeoLocationNonExistingError)
}def validate(listing: Listing, emptyID: Boolean): ValidatedNel[PropertyValidationError, Listing] = {
Apply[ValidatedNel[PropertyValidationError, ?]].map4(
validateId(listing.id, emptyID).toValidatedNel,
ContactValidator.validate(listing.contact),
AddressValidator.validate(listing.address),
validateGeoLocation(listing.location).toValidatedNel
) {
case (id, contact, address, location) => Listing(id, contact, address, location)
}
}
Considerations about the previous code:
- This validator contains inline validations, like the geolocation
validation and the id
validation.
- It is composed by other validators classes, like the ContactValidator
or the AddressValidator
.
- The validate
function is the main function of the validation. It creates an Applicative using the Apply
constructor. The container in this case is ValidatedNel
, to aggregate PropertyValidationErrors
.
- We can see that it is applied the function map4
, that belongs to SemiGroupal
. SemiGroupal captures the idea of composing independent effectful values. This is exactly what we need in our application.
- In the validateGeoLocation
returns a Valid
or Invalid
value. We can see that the function returns a Validated type.
Source Code
The complete source code can be found here. The source code contains a Rest API that uses the Cats form validation described above. It also contains JUnit tests that validate the different parts of the application.
Conclusions
Cats, Monads, Applicative, Semigroupal… all of this could seem a bit cryptic and chaotic at first. With suitable application however, it can help you to solve complex scenarios.
I hope this post will inspire you to start using Cats in your codebase. If you’d like to learn more I highly recommend reading Scala with Cats by Underscore. The book is comprehensive and explains well the subtleties of using Cats.
Originally published at https://dvirgiln.github.io.