Comprehensive Data Validation in Scala

Anzori (Nika) Ghurtchumelia

19th July 2023

Data validation is not new thing in software development, it’s been here all along and will continue to stay with us. Comprehensive data validation leads to achieving the following goals:

  • Disallowing anyone to represent the illegal state in our apps
  • Accumulating all of the exact reasons for the validation failure (Can not be achieved by using Either[E, A] since it’s a monadic structure — supports short circuiting in flatMap calls and returns to the caller as soon as it encounters the first Left case, so we need something different)

In this blog post we will solve the practical problem — We will write the fully fledged validation for the case where the user is trying to create the password for his/her profile.

Our goal is to accumulate validation errors so that the user is able to view all the faults on the UI while at the same time trying to fix the password.

Please keep in mind that we need to make sure that the provided password conforms to the security standards i.e it must be validated against four rules imposed by our business case:

  • It must be non empty (who would’ve thought?! 😃)
  • It must contain at least one uppercase character
  • It must contain more than 8 characters to be classified as sufficiently long enough
  • It must contain at least one of these special symbols: !@#$%^&*()

We will make us of the famous cats library and more importantly the constructions we have under cats.data package, things such as: Validated (addresses the limitations of Either[E, A]), NonEmptyList and a special type alias associated with the aforementioned.

First of all, let’s get familiar with Validated and NonEmptyList and then proceed with the implementation.

Validated is a sum type data structure which is represented either as Valid or Invalid that wraps the underlying data that is subject to validation.

A simple example demonstrating the hierarchy of Validated:

sealed trait Validated[+E, +A] { // a bunch of abstract methods } object Validated { final case class Valid(data: A) extends Validated[Nothing, A] final case class Invalid(error: E) extends Validated[E, Nothing] }

So, clearly the left part +E is dedicated to hold validation errors while the right part +A is used to store the passed values.

At the first glance it looks pretty much the same as Either[+E, +A] with its Left and Right parts but it works in a different way in practice.

A simple example of creating Valid data:

import cats.data.Validated val validPass: Validated[Nothing, String] = Validated.Valid("MyPassword!2#")

A simple example of creating Invalid data:

import cats.data.Validated val invalidPass: Validated[String, Nothing] = Validated.Invalid("The password is invalid due to ...")

We can also use the extension methods that helps us to wrap anything with `Validated

import cats.syntax.all._ val validPass: Validated[Nothing, String] = "MyPassword!2#".valid val invalidPass: Validated[String, Nothing] = "The password is invalid due to ...".invalid

Simple bruv, innit?

Let’s move on NonEmptyList. As the name suggest it’s basically a non empty list. Sometimes NonEmptyList is referred as Nel by geeks (pun intended on Nil which is an empty list 😄) and it’s based on singly linked list implementation.

A simple demonstration of the NonEmptyList usage:

import cats.data.NonEmptyList val oneElementList: NonEmptyList[Int] = NonEmptyList.one(1) val maybeNonEmptyList1: Option[NonEmptyList[Int]] = NonEmptyList.fromList(Nil) // None, due to Nil param val maybeNonEmptyList2: Option[NonEmptyList[Int]] = NonEmptyList.fromList(List(1, 2, 3)) // Some(...) due to List(1, 2, 3) param NonEmptyList.fromListUnsafe(Nil) // throws Exception due to Nil param

Even simpler, innit?

And now let’s see the very type alias which suits our needs:

type ValidatedNel[+E, +A] = Validated[NonEmptyList[E], A]

So, after applying the validation rules we’re either getting the NonEmptyList of domain errors — E or underlying data — A that has passed the validation rules.

Great! Now, let’s create a domain model for password and possible error hierarchy ADT associated with it:

final case class Password(value: String) extends AnyVal // value class object Password { // validation error ADT sealed trait ValidationError object ValidationError { case object IsEmpty extends ValidationError case object LacksUppercaseChar extends ValidationError case object IsShort extends ValidationError case object LacksSpecialSymbol extends ValidationError } }

Looks pretty straightforward so far, now let’s design a protocol which describes the password validation on the high level:

import Password.ValidationError trait PasswordValidations { def validate(password: String): ValidatedNel[ValidationError, Password] }

And finally let’s write the implementation in the companion object of PasswordValidations:

def create: PasswordValidations = password => { import Password.ValidationError import Password.ValidationError._ def validate( condition: Boolean, failureReason: ValidationError ): ValidatedNel[ValidationError, Unit] = if (!condition) failureReason.invalidNel else ().valid def isNonEmpty = validate(password.nonEmpty, IsEmpty) def containsUpperCaseCharacter = validate(password.exists(_.isUpper), LacksUppercaseChar) def isLong = validate(password.length > 8, IsShort) def hasSpecialSymbol = validate( password.exists(c => "!@#$%^&*()".contains(c.toString)), LacksSpecialSymbol ) // Here we need to call the following methods: // 1. isNonEmpty // 2. containsUpperCaseCharacter // 3. isLong // 4. hasSpecialSymbol // and accumulate errors in the Invalid channel if there are any // or return the successful password wrapped by Valid constructor ??? } }

We’re almost there, now we need a way to use all of our four inner validation functions and:

  • if at least one or more of them fails — return concatenated Invalid(NonEmptyList[ValidationError](...))
  • if none of them fails — return Valid(Password(...))

There are a few ways to achieve this but I’ll show you my favourite approach that involves using .tupled extension method which is defined on tuples of arbitrary values that are wrapped by something like Option, Either or in our case Validated.

Under the hood .tupled is using Semigroupal typeclass and product operation to achieve the following:

import cats.syntax.all._ val maybe1 = Option(1) val maybe2 = Option(2) val result: Option[(Int, Int)] = (maybe1, maybe2).tupled // Some(1, 1)

In our case the tuple will contain the inner function calls with the return type of: ValidatedNel[ValidationError, Unit].

So, we’d write something like:

import cats.syntax.all._ // .tupled concatenates the NonEmptyList[ValidationError] if they are Invalid // and tuples the Unit values if are Valid val result: ValidatedNel[ValidationError, (Unit, Unit, Unit, Unit)] = ( isNonEmpty, containsUpperCaseCharacter, isLong, hasSpecialSymbol ).tupled

But.. it’s not enough, we want to have this type of signature in the end: ValidatedNel[ValidationError, Password], so we could easily map the result like:

result.map(_ => Password(password)) // ValidatedNel[ValidationError, Password]

or use .as extension method which makes it even more succinct and readable:

result.as(Password(password)) // ValidatedNel[ValidationError, Password]

So, now let’s put it all together:

object PasswordValidations { def create: PasswordValidations = password => { import Password.ValidationError import Password.ValidationError._ def validate( condition: Boolean, failureReason: ValidationError ): ValidatedNel[ValidationError, Unit] = if (!condition) failureReason.invalidNel else ().valid def isNonEmpty = validate(password.nonEmpty, IsEmpty) def containsUpperCaseCharacter = validate(password.exists(_.isUpper), LacksUppercaseChar) def isLong = validate(password.length > 8, IsShort) def hasSpecialSymbol = validate( password.exists(c => "!@#$%^&*()".contains(c.toString)), LacksSpecialSymbol ) ( isNonEmpty, containsUpperCaseCharacter, isLong, hasSpecialSymbol ).tupled.as(Password(password)) } }

Now let’s write the tests for our validation service:

Expecting Invalid(NonEmptyList(IsEmpty, LacksUppercaseChar, IsShort, LacksSpecalSymbol)) since all of the four rules fail:

import Password.ValidationError._ val service = PasswordValidations.create val expected = NonEmptyList.fromListUnsafe { List(IsEmpty, LacksUppercaseChar, IsShort, LacksSpecialSymbol) }.invalid assert(service.validate("") == expected)

Expecting Invalid(NonEmptyList(LacksUppercaseChar, IsShort)):

import Password.ValidationError._ val service = PasswordValidations.create val expected = NonEmptyList.fromListUnsafe { List(LacksUppercaseChar, IsShort) }.invalid assert(service.validate("pa$$") == expected)

Expecting Valid(Password(...)) since it conforms to all of the four rules:

import Password.ValidationError._ val service = PasswordValidations.create val pass = "Very$trongPassword123" val expected = Password(pass).valid assert(service.validate(pass) == expected)

I hope you enjoyed it and learned something new today!


Follow Anzori on social media to stay up to date with his latest content!

Subscribe to receive the latest Scala jobs in your inbox

Receive a weekly overview of Scala jobs by subscribing to our mailing list

© 2024 ScalaJobs.com, All rights reserved.