{{announcement.body}}
{{announcement.title}}

http4s: JSON Request Validator

DZone 's Guide to

http4s: JSON Request Validator

The main problem of 99.99 percent of web apps is that they have users.

· Integration Zone ·
Free Resource

There are three things a software developer can watch forever: fire, water, and request validators. In the previous blog post, I demonstrated how to develop a simple REST API application with http4s. But that app has a drawback  — the user can send any invalid data to the API and it will be processed with no error messages.

You may also like: Implementing Validation for RESTful Service With Spring Boot

The Problem

The main problem of 99.99 percent of web apps is that they have users (and feel free to tweet this). As a result, there is a huge human factor, which leads to a tremendous amount of inappropriate interactions with web applications. One of my favorites is invalid data input. For example, the user may enter "blablabla" in an email field, "MyNickName" in a phone number field or even leave a form field empty when it's required.

It's a fact that every data model in any application has its own validation rules. Let's consider a book model:

type Title = String
type Author = String

case class Book(title: Title, author: Author)


For simplicity, let's assume that title and author have only one requirement — not empty field. What does it mean from a REST API point of view? In case when any of these fields is empty, a response should be 400 Bad Request; also, it should contain an explanation of validation errors:

[
    {
        "fieldName": "title",
        "message": "Must not be empty"
    },
    {
        "fieldName": "author",
        "message": "Must not be empty"
    }
]


With such a response, it's easy to reflect on UI what exactly went wrong.

JSON Validation in http4s

I spent plenty of time while investigating how it's better to organize JSON request validation. In long Gitter discussions and StackOverFlow threads, I came up with the following solution. Firstly, we need to declare some data models that describe possible validation errors:

import cats.data.NonEmptyList

type FieldName = String
type Message = String

final case class FieldError(fieldName: FieldName, message: Message)

type ValidationResult = Option[NonEmptyList[FieldError]]


Now, we can go ahead and use these models to define two validation abstractions:

trait Validator[T] {
  def validate(target: T): ValidationResult
}

trait FieldValidator[T] {
  def validate(field: T, fieldName: FieldName): ValidationResult
}


Here, Validator[T] is needed for describing a certain validator for a particular case class. Whereas FieldValidator[T] aims to describe a certain validation rule for a particular field of a case class. Now, we can make the next logical step and create some real field validators. Let's assume that we want to ensure that a String type field can not be empty:

case object NotEmpty extends FieldValidator[String] {
  def validate(target: String, fieldName: FieldName) =
    if (target.isEmpty) NonEmptyList.of(FieldError(fieldName, "Must not be empty")).some else None
}


As you can see, the NotEmpty case class describes this rule. What about a more flexible example? Let's say we need to validate a String type field by its length:

case class WithLength(min: Int, max: Int) extends FieldValidator[String] {
  def validate(target: String, fieldName: FieldName) =
    if (target.length < min || target.length > max)
      NonEmptyList.of(FieldError(fieldName, s"Length must be between $min and $max")).some else None
}


By using the same approach, you can create any custom rule for an arbitrary type.

Now, it's time to return to the books REST API app and ensure that we don't allow to process a Book model with empty fields.

object Book {
  import cats.implicits._
  import FieldValidator.strings._
  implicit val validator: Validator[Book] = (target: Book) => {
    NotEmpty.validate(target.title, "title") |+|
    NotEmpty.validate(target.author, "author")
  }
}


From the code snippet, you see that I added Validator[Book] to the Book companion object. Therefore, the validation rule is always provided with the model itself. Just for demonstration purposes, I want to show how it's easy to add Book model validation to http4s routes:

...
case req @ POST -> Root / "books" =>
  req.decode[Book] { book =>
    Book.validator.validate(book) match {
      case None =>
        bookRepo.addBook(book).flatMap(id =>
          Created(Json.obj(("id", Json.fromString(id.value))))
        )
      case Some(errors) => BadRequest(errors)
    }
  }
...


Voila! Your dinner is served!

This approach is pretty convenient from a code maintenance point of view, plus it scales well, by introducing reusable FieldValidator[T] implementations.

Summary

In this article, I described how you can set up a consistent approach for data validation. It was inspired by conversations with the Scala community and may have some disadvantages, which I haven't notice for some reason. So I'll be happy to read any feedback.

Further Reading

Implementing Validation for RESTful Service With Spring Boot

How to Use Validate JSON Schema and Message Enricher in Mule

[DZone Refcard] Core JSON

Topics:
http4s ,validation ,integration ,scala ,api ,rest api ,json

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}