[apss_share counter='0' total_counter='1']

Introduction to Validation on Scala

As a developer, you’ve probably had to write some logic with multiple validations on multiple variables. For example, when validating fields in a form, or validating parameters sent to an API.

 

There are many ways to arrange this code, ranging from a long mess of “if” statements to more subtle polymorphism using the strategy pattern.

 

Scala offers a functional approach to this problem, and as you’ll soon see it’s a very clean, clear and powerful approach.

 

Considering the following scenario, we have to validate a request coming to our web service. Our web service will need to assert some crucial information about the sender. Information such as, the IP and location of the sender and the device from which they’re sending their request.

 

Let’s define a simple model for our service API:

 

// incoming request parameters
final case class ClientData(ip: String, time: Instant)
final case class LocationData(longitude: Long, latitude: Long)
final case class DeviceData(userAgent: String)

 

//the data model of our service
final case class Data(client: ClientData, location: LocationData, device: DeviceData)

 

To validate the incoming request parameters, we’ll define these validation functions:

 

def validateClientData(ip: String, time: Instant): ClientData
def validateLocationData(longitude: BigDecimal, latitude: BigDecimal): LocationData
def validateDeviceData(userAgent: String): DeviceData

 

These functions take request parameters and build validated model classes. But what about failures? Here are some failure object definitions:

 

sealed trait Failure
final case object InvalidClient extends Failure
final case object InvalidLocation extends Failure
final case object InvalidDevice extends Failure

 

We have our definitions, and now we need to start using these validation functions.

 

Let’s start by validating only what we need and stopping on the first failure.

 

Sequencing validations

Our current validations’ return types don’t reflect the possibility of failure, so we’ll incorporate them using the Either monad. It defines the Left subtype for failure and the Right subtype for success. Either’s a monad (therefore implements map and flatMap in scala), so we can use for comprehension on it.

 

for {
clientData <- validateClientData(“1.1.1.1”, Instant.now())
locationData <- validateLocationData(10.10, 10.10)
deviceData <- validateDeviceData(“Mozilla/5.0 (…) Safari/537.36”) } yield (clientData, locationData, deviceData) >> Right(Data(ClientData(1.1.1.1,…),LocationData(10.1,10.1),DeviceData(Mozilla/5.0 …)))

 

When one of the validations fails, the whole statement fails and we will receive the Failure object inside the monadic structure of your choice (e.g Left).

 

for {
clientData <- validateClientData(“1.1.1.1”, Instant.now())
locationData <- validateLocationData(9000.0, 10.10) /* ⇐ this is not valid */
deviceData <- validateDeviceData(throw new RuntimeException) /* this is not evaluated */ } yield Data(clientData, locationData, deviceData) >> Left(InvalidLocation)

 

What if we need to save all the errors?

In some cases, failing fast is not desirable. In certain situations, we would like to accumulate all the failures. Let’s say we need to send the client a response with all the parameters that have failed.

 

We can think of several approaches to this problem.

 

Naïve approach

We can use the same Either structure as before, and just lazily evaluate it before aggregating the results:

 

lazy val clientData = validateClientData(“1.1.1.1”, Instant.now())
lazy val locationData = validateLocationData(9000.10, 10.10) /* ⇐ this is not valid */
lazy val deviceData = validateDeviceData(null) /* ⇐ also not valid */

 

{
for {
r <- clientData
l <- locationData
d <- deviceData } yield Data(r, l, d) } getOrElse { List(clientData, locationData, deviceData) withFilter ( _.isLeft) map (_.left.get) } >> List(InvalidLocation, InvalidDevice)

 

This approach works well enough, but it’s a bit cumbersome, repetitive and the types don’t really reflect the business logic.

 

Applicative Validation

Applicative

An Applicative Functor (or simply Applicative) implements pure (a constructor) and apply functions, and follows the laws of Associativity, Left Identity and Right Identity. The apply function is defined as :

apply[A,B](f:F[A⇒B])⇒F[A]⇒F[B]

 

For our use case, we can see it basically as a multiple parameter mapping function. This is how we map over a tuple.

 

Validation

If we don’t have dependencies between validations, we can use the Validated Applicative Functor. We will use cats’ applicative validation for my example, but scalaz provides a very similar construct (Validation).

 

We will use a variation of Validated called ValidatedNel. The Nel suffix means that the Failures will be aggregated into the self-described NonEmptyList structure, i.e. simply collected into a non-empty list.

 

import cats.syntax.all._

 

type Result[A] = ValidatedNel[Failure, A]

 

override def validateClientData(ip: String, time: Instant): Result[ClientData] =
if (!Ip.valid(ip)) InvalidClient.invalidNel else ClientData(ip, time).validNel

 

override def validateLocationData(x: BigDecimal, y: BigDecimal): Result[LocationData] =
if (!Location.valid(x,y)) InvalidLocation.invalidNel else LocationData(x, y).validNel

 

override def validateDeviceData(userAgent: String): Result[DeviceData] =
if (!UserAgent.valid(userAgent)) InvalidDevice.invalidNel else DeviceData(userAgent).validNel

 

(
validateClientData(“1.1.1.1”, Instant.now()),
validateLocationData(10.10, 10.10),
validateDeviceData(“Mozilla/5.0 (…) Chrome/61.0.3163.100 Safari/537.36”)
) mapN (Data(_, _, _))
>> Valid(Data(ClientData(1.1.1.1,…),LocationData(10.1,10.1),DeviceData(Mozilla/5.0 …)))

 

If some of our validations fail:

 

(
validateClientData(“1.1.1.1”, Instant.now()),
validateLocationData(9000.0, 10.10), /* ⇐ this is not valid */
validateDeviceData(null) /* ⇐ also not valid */
) mapN (Data(_, _, _))
>> Invalid(NonEmptyList(InvalidLocation, InvalidDevice))

 

Looks good. Our syntax is concise, the validation functions’ return types reflect the business logic, and the failures are aggregated into a well-defined type. Let’s see what’s going on here:

  • We changed our return types to ValidatedNel.
  • We took a tuple of validations and mapped over it. The mapN function being a mapping function over N parameters.
  • If any of the validations fail, we get a collection of all the failures.

Note the Result type definition in the second row. It’s not just for convenience – it is very important if we want to use cats’ mapN. The implicit conversion that contains mapN looks for a tuple with the type of F[_].

 

It means that we must define such a type if we want to use this syntax.

 

It also means that all our Failure types must be subtypes of the same parent.

 

TL;DR

Scala provides us with the for syntax – a very concise and readable syntax that can also be used in fail-fast validations.

 

However, in other cases, we would need to validate independent statements and save all of the failed validations (for example, if we need to send all the failed fields to a client). In these cases we won’t have to reinvent the wheel, as we can use an existing structure – Validated – that is implemented in both cats and scalaz (as Validation).


Daniel Krupitsky, Backend & Senior Server Developer

Contact Us



Imprint

Fyber N.V.

Office Address:
Johannisstraße 20
10117 Berlin
Germany
Phone: +49 30-6098555-0
Fax: +49 30-6098555-35
Email: info@fyber.com

Managing Directors: Ziv Elul, Daniel Sztern, Yaron Zaltsman, Crid Yu

Statutory seat: Amsterdam, The Netherlands,
Kamer van Koophandel KvK-Nr. 54747805
German Branch Office: Amtsgericht Charlottenburg, HRB 166541

VAT ID No.: DE289234185
LEI: 894500D5B6A8E1W0VL50

Editorial responsibility for content under § 55 II RStV: Gerd Mittmann, Johannisstrasse 20, DE-10117 Berlin

Disclaimer

Fyber is not responsible for any content of third-party websites which can be accessed via links from Fyber’s website. Fyber does not control any of these sites and expressly dissociates itself from their content.

Copyright

All rights reserved. Reproduction of any content on Fyber’s website as well as storage and usage of such content on optical or electronic data carriers only upon prior written consent of Fyber. Unwarranted utilization of such content in whole or in part by third parties is strictly prohibited.