Validation of Request Body Akka HTTP

The question is how to validate a HTTP request body using Akka HTTP directive? Of course we can use for this reason validate directive, but it has one drawback which I described in my previous post about a model validation in Akka HTTP. You may want to use require method, but it is not so functional as well. Today I want to show another way for validation of HTTP request body in Akka.

Validation problem

Let’s assume we have a REST endpoint which works with a following data model:

case class Contact(name: String, email: String, age: Option[Int])

As you see this is a simple case class. Of course we want to be sure that all of data is valid, e.g. age is not negative.

How to implement constraints for case class fields in a context of Akka HTTP? We just want to have a nice response for situations when some of fields or all of them have invalid values. We need to send back to a client something like this:

[
  {"fieldName": "name", "errorMessage": "name length must be between 2 and 30 characters"},
  {"fieldName": "email", "errorMessage": "email must be valid"},
  {"fieldName": "age", "errorMessage": "age must be between 16 and 99"},
]

After the problem is highlighted, a solution need to be find for it.

Akka HTTP validation directive

Firstly we need to declare a standard data classes for validation domain:

final case class FieldErrorInfo(name: String, error: String)
final case class ModelValidationRejection(invalidFields: Seq[FieldErrorInfo]) extends Rejection

Then we can create a trait which contains an abstraction of validation logic:

trait Validator[T] extends (T => Seq[FieldErrorInfo]) {

  protected def validationStage(rule: Boolean, fieldName: String, errorText: String): Option[FieldErrorInfo] =
    if (rule) Some(FieldErrorInfo(fieldName, errorText)) else None

}

The validationStage(rule: Boolean, fieldName: String, errorText: String) represents the smallest piece of a model validation process. It has 3 arguments. The first one is rule, it is responsible for some validation constraint which you want to apply to a model field. The second one is fieldName, it’s needed for a validation response. The third one is errorText, it’s used for a description of validation error.
Even if not all of the fields are clear for understanding, read further, I’d definitely understand the concept on example.

Finally here is a validation directive:

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.Directive1
import akka.http.scaladsl.server.Directives._
import spray.json.DefaultJsonProtocol

object ValidationDirective extends SprayJsonSupport with DefaultJsonProtocol {

  import akka.http.scaladsl.server.Rejection

  implicit val validatedFieldFormat = jsonFormat2(FieldErrorInfo)

  def validateModel[T](model: T)(implicit validator: Validator[T]): Directive1[T] = {
    validator(model) match {
      case Nil => provide(model)
      case errors: Seq[FieldErrorInfo] => reject(ModelValidationRejection(errors))
    }
  }

}

I use spray-json for marshaling / unmarshaling. You can use any alternative and it will work any way.

Validation Example

The best way to see how the directive works is to write a test for it or a couple of tests. Moreover we will ensure that it works fine. Firstly I suggest to see how a particular validator acts and then inject it in Akka HTTP directives.

case class Contact(name: String, email: String, age: Option[Int])

object ContactValidator extends Validator[Contact] {

  //Predefined rules. They can be placed somewhere else in order to be accessible for other validators
  private def nameRule(name: String) = if (name.length < 2 || name.length > 30) true else false
  private def emailRule(email: String) = if ("""\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z""".r.findFirstMatchIn(email)
    .isEmpty) true else false
  private def ageRule(age: Option[Int]) = if (age.isDefined && (age.get < 16 || age.get > 99)) true else false

  override def apply(model: Contact): Seq[FieldErrorInfo] = {

    val nameErrorOpt: Option[FieldErrorInfo] = validationStage(nameRule(model.name), "name",
      "name length must be between 2 and 30 characters")

    val emailErrorOpt: Option[FieldErrorInfo] = validationStage(emailRule(model.email), "email", "email must be valid")

    val ageErrorOpt: Option[FieldErrorInfo] = validationStage(ageRule(model.age), "age",
      "age must be between 16 and 99")

    (nameErrorOpt :: emailErrorOpt :: ageErrorOpt :: Nil).flatten
  }

}

That’s how you can create a validator for Contact model. Notice that if you have many models and some of validation rules are common, you’d better to hold them somewhere in a more general place (validation rules trait or object).
What about testing?

import org.scalatest.FunSuite

class ContactValidatorSuite extends FunSuite {

  test("positive validation #1") {
    val contact = Contact("Jo", "jo@example.com", None)
    assert(ContactValidator(contact) === Seq())
  }

  test("positive validation #2") {
    val contact = Contact("PrettyLongNameAsForContactOkOk", "good_email@example.com", Some(16))
    assert(ContactValidator(contact) === Seq())
  }

  test("negative validation #1") {
    val contact = Contact("a", "Aexample.com", Some(15))
    assert(ContactValidator(contact) === Seq(
      FieldErrorInfo("name", "name length must be between 2 and 30 characters"),
      FieldErrorInfo("email", "email must be valid"),
      FieldErrorInfo("age", "age must be between 16 and 99")
    ))
  }

  test("negative validation #2") {
    val contact = Contact("PrettyLongNameAsForContactBadIdea", "A@examplecom", Some(100))
    assert(ContactValidator(contact) === Seq(
      FieldErrorInfo("name", "name length must be between 2 and 30 characters"),
      FieldErrorInfo("email", "email must be valid"),
      FieldErrorInfo("age", "age must be between 16 and 99")
    ))
  }

}

Ok.

Now we see that validator works as expected. It’s time to see it in action in a context of Akka HTTP.

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.model.{HttpResponse, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.testkit.ScalatestRouteTest
import org.scalatest.FunSuite
import spray.json.DefaultJsonProtocol

class ContactValidatorAPISuite extends FunSuite with ScalatestRouteTest with SprayJsonSupport with DefaultJsonProtocol {

  implicit val contactFormatter = jsonFormat3(Contact)

  import com.vidiq.http.validator.ValidationDirective._

  //Required implicit validator declaration
  implicit val contactValidator = ContactValidator

  val fakeAPI = pathPrefix("contacts") {
    post {
      entity(as[Contact]) { contact =>
        validateModel(contact).apply { validatedContact =>
          complete(HttpResponse(StatusCodes.OK))
        }
      }
    }
  }

  test("positive validation") {
    Post("/contacts", Contact("Jo", "jo@example.com", None)) ~> fakeAPI ~> check {
     assert(status === StatusCodes.OK)
    }
  }

  test("negative validation") {
    Post("/contacts", Contact("a", "Aexample.com", Some(15))) ~> fakeAPI ~> check {
      assert(rejection === ModelValidationRejection(Seq(
        FieldErrorInfo("name", "name length must be between 2 and 30 characters"),
        FieldErrorInfo("email", "email must be valid"),
        FieldErrorInfo("age", "age must be between 16 and 99")
      )))
    }
  }

}

And do not forget to handle ModelValidationRejection.

Summary

Well, I presented a solution to handle a model (case classes) in Akka HTTP. Of course it may be not so brilliant as many of you want, but at least it solves the problem. For sure you have own thoughts how the validation should be implemented. I’d be really glad to read your comments regarding this.

About The Author

Mathematician, programmer, wrestler, last action hero... Java / Scala architect, trainer, entrepreneur, author of this blog

Close