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

Akka HTTP: Case Class Validation Field by Field

DZone's Guide to

Akka HTTP: Case Class Validation Field by Field

If you've worked with Akka HTTP for a while, you should definitely know that it has multiple methods of model validation. Here we talk about HTTP request body validation.

· Integration Zone ·
Free Resource

SnapLogic is the leading self-service enterprise-grade integration platform. Download the 2018 GartnerMagic Quadrant for Enterprise iPaaS or play around on the platform, risk free, for 30 days.

If you've worked with Akka HTTP for a while, you should definitely know that it has multiple ways for model validation. I'm talking about HTTP request body validation. Probably in 99% of cases, you would like to ensure that the user sends something meaningful to the server. So for this exact purpose, Akka HTTP provides validation mechanisms. But what if you want to send information back to the client side about all invalid fields?

Overview of Existing Validators

When I first time started looking for handy validation mechanisms that Akka HTTP has, I pretty quickly found the validate directive. Of course, you know it well:

def validate(check: => Boolean, errorMsg: String): Directive0

It's extremely straightforward and pragmatic. You pass the check argument and errorMsg, and if check is false, the directive generates ValidationRejection with the appropriate error message.

Not bad, not bad. But it's hard to apply this approach for case class validation, when you want to receive the entire list of errors for each invalid field.

What else does Akka HTTP have for validation? Right! A case class extraction in combination with therequire method as the validator.

I was so happy to see this code snippet:

case class Color(name: String, red: Int, green: Int, blue: Int) {
  require(!name.isEmpty, "color name must not be empty")
  require(0 <= red && red <= 255, "red color component must be between 0 and 255")
  require(0 <= green && green <= 255, "green color component must be between 0 and 255")
  require(0 <= blue && blue <= 255, "blue color component must be between 0 and 255")
}

Until I understood that in the case of a "require" statement failure, it would immediately send ValidationRejection to the client with the recent error, instead of:

[
  {"fieldName": "red", "errorMessage": "red color component must be between 0 and 255"},
  {"fieldName": "name", "errorMessage": "color name must not be empty"}
]

Hell. In some moment of time, I started to think that it's just impossible to achieve what I want with native Akka HTTP. StackOverflow, blogs, and finally the Akka Gitter channel... By the way, thanks to the admins of Akka Gitter, they suggested to me one way to do what I want with the help of ScalaTest checkers, or something like that.

Sorry, dear hakkers, but finally I decided to write my own solution. I wanted something really compact, simple, understandable, extensible (and the rest of the characteristics which make open source libs outstanding).

Unfortunately, I'm not so talented a programmer, so we have what we have.

mini-nano-micro Directive for Case Class Validation

Do you want to dive into the source code? Check out the GitHub repo. An example of the directive usage is in the test suite.

So how do I see validation of case classes in a Akka HTTP context? Let's start from preconditions.

First, we need to define some validation rules for a case class. These rules may be reused later and composed with other rules. For this reason, we can use such a line of code:


final case class FieldRule[-M](fieldName: String, isInvalid: M => Boolean, errorMsg: String)

Then we want to be dynamic enough in order to avoid boilerplate code. Java reflections can help us with this challenge:


def caseClassFields[M <: Any](obj: AnyRef): Seq[(String, M)] = {
  val metaClass = obj.getClass
  metaClass.getDeclaredFields.map {
    field => {
      field.setAccessible(true)
      (field.getName, field.get(obj).asInstanceOf[M])
    }
  }
}

Now we can create a validation directive:


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

implicit val validatedFieldFormat = jsonFormat2(FieldErrorInfo)

def validateModel[T, M <: Any](model: T, rules: FieldRule[M]*): Directive1[T] = {
  import scala.collection.mutable.Set
  val errorsSet: Set[FieldErrorInfo] = Set[FieldErrorInfo]()
  val keyValuePairs: Seq[(String, M)] = caseClassFields(model.asInstanceOf[AnyRef])
  Try {
    rules.map { rule =>
      keyValuePairs.find(_._1 == rule.fieldName) match {
        case None => throw new IllegalArgumentException(s"No such field for validation: ${rule.fieldName}")
        case Some(pair) => {
          if (rule.isInvalid(pair._2)) errorsSet += FieldErrorInfo(rule.fieldName, rule.errorMsg)
        }
      }
    }
    errorsSet.toSet[FieldErrorInfo]
  } match {
    case Success(set) => if (set.isEmpty) provide(model) else reject(ModelValidationRejection(set))
    case Failure(ex) => reject(ValidationRejection(ex.getMessage))
  }
}

All together, without imports, it takes ~40 lines of code. I'm not sure that this solution is optimal enough, that it's elegant and all of the Scala features are used as needed. But I hope to hear what can be done better, what can be improved, and how the code can be more universal.

My nearest two goals are to unbound the spray-json dependency (make this directive open for any serialization provider) and implement a way to pass validation rules not only one by one, but in a Seq form as well.

Summary

Of course, you can take a look at the source code. I'll be happy to read your impressions about this directory in the comments. Any suggestions are welcome.

With SnapLogic’s integration platform you can save millions of dollars, increase integrator productivity by 5X, and reduce integration time to value by 90%. Sign up for our risk-free 30-day trial!

Topics:
integration ,akka ,http ,validation

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}