Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Akka HTTP: Another One Validation Direction

DZone's Guide to

Akka HTTP: Another One Validation Direction

Learn how to validate an HTTP request body using Akka HTTP directives. This simple example covers common problems with validation and a solution.

· Java Zone ·
Free Resource

Verify, standardize, and correct the Big 4 + more– name, email, phone and global addresses – try our Data Quality APIs now at Melissa Developer Portal!

The question today is how to validate an HTTP request body using Akka HTTP directives. Of course, we can use the 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 the require method, but it is not very functional. Today, I want to show another way to validate HTTP request bodies in Akka.

Validation Problem

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

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


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

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

[
    {
        "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 needs to be found for it.

Akka HTTP Validation Directive

Firstly, we need to declare standard data classes for the 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 that 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 three arguments. The first one is rule, which is responsible for some validation constraint that you want to apply to a model field. The second one is fieldName, which is needed for a validation response. The third one is errorText, which is used for a description of the validation error.

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.

Validation Example

The best way to see how the directive works is to write a test or two for it. Moreover, we will ensure that it works fine. Firstly, I suggest seeing how a particular validator acts and then inject it in our 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 the Contact model. Notice that if you have many models and some validation rules are common, you’d better hold them 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 the validator works as expected. It’s time to see it in action in the 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 not be brilliant as many of you want, but at least it solves the problem. You probably have your own thoughts on how validation should be implemented. I’d be really glad to read your comments regarding that.

Developers! Quickly and easily gain access to the tools and information you need! Explore, test and combine our data quality APIs at Melissa Developer Portal – home to tools that save time and boost revenue. Our APIs verify, standardize, and correct the Big 4 + more – name, email, phone and global addresses – to ensure accurate delivery, prevent blacklisting and identify risks in real-time.

Topics:
java ,akka http ,validation ,scala ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}