Fully Customized Json Validators for the Play 2 Framework: Explanation and Example

Fully Customized Json Validators for the Play 2 Framework: Explanation and Example

Last updated:
Fully Customized Json Validators for the Play 2 Framework: Explanation and Example
Source
Table of Contents

Play's Json Library is, in my opinion, one of the best features of the Play 2 Framework. So much so that I always use it even in apps where I don't use Play at all.

The usual pattern is to create a couple of Case Classes, Readers and Writers to convert your models to/from Json format.

To convert to/from json, you need case classes, readers and writers

You probably also use the validate[T] method to validate a given JsValue against a case class; it is very handy because, in case validation fails, you get the error messages all nicely formatted for you, and you can output that:

def myControllerAction() = Action(parse.json){ request =>
  // using parse.json the request body
  // will be a JsValue
  request.body.validate[MyCaseClass] match{
    case success: JsSuccess[MyCaseClass] => {
      //process the request
    }
    case JsError(error) => BadRequest(JsError.toJson(error))
  }
}

You can add a couple of extra filters to your Readers and Writers to validate each individual field but you are a little bit constrained as to what you can do.

case Class User(name:String,age:Int)

// names must be alphanumeric
def validName(name:String) = name.matches("""(\d|\w|_)+""")

// ages must be positive
def positiveInt(i:Int) => i > 0

object User{
  implicit val reads: Reads[User] = (
    (JsPath \ "name").read[String](validName) and
    (JsPath \ "age").read[Int](positiveInt))(User.apply _)
  )
}

What if you want to allow users whose name is "john" and have negative age?

To write a fully customized extractor, (for instance, what if you want to allow users whose name is "john" to have negative age?) you can use the following approach, gaining full control of how JsSuccess and JsError are returned, as well as the JsPath that will be indicated as having been the source of that error.

This is perfect to have your model validations return properly structured error messages that can be easily parsed on the frontend.

implicit object UserReads extends Reads[(String, Int)] {
  def reads(json: JsValue): JsResult[(String, Int)] = json match {
    case jsval: JsValue => {

      val maybeName = (jsval \ "name").asOpt[String]
      val maybeAge = (jsval \ "age" ).asOpt[Int]

      maybeName match {
        case Some(name) => {
          maybeAge match {
            case Some(age) => {
              // let's pretend this function is defined somewhere
              if(customValidate(user,age))  JsSuccess((name, age))
              else JsError(Seq(JsPath() -> Seq(ValidationError("error.custom.bad_user_data"))))
            }
            case None => JsError(Seq(JsPath() \ "age" -> Seq(ValidationError("error.path.missing"))))
          }
        }
        case None => JsError(Seq(JsPath() \ "name" -> Seq(ValidationError("error.path.missing"))))
      }
    }
    case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsobject"))))
  }
}

Validate Against a Trait

Suppose you want to verify that some json object is a valid Car. But Car is not a class, but a trait that classes can extend.

It's also possible to validate against a trait; view a working example here

In this example, we have a Car trait and two concrete elements, a case object called MercedesBenz and a case class NormalCar(model: String) and we want to consider some json valid if it validates against either of the two objects.

import play.api.data.validation.ValidationError
import play.api.libs.json._
import play.api.libs.json.Reads._

// the trait and extending classes/objects
sealed trait Car
case object MercedesBenz extends Car
case class NormalCar(model: String) extends Car

// the reader for the trait
implicit object CarReads extends Reads[Car] {
  // as in the other example, you need to define 
  def reads(json: JsValue): JsResult[Car] = json match {
    // a car can be a single json string 
    case JsString(s) => if (s == "mercedes") JsSuccess(MercedesBenz) else JsError(Seq(JsPath() -> Seq(ValidationError("error.invalid"))))
    // or a json object with a "model" attribute 
    case JsObject(obj) => obj.get("model") match {
      case Some(model) => model match {
        case JsString(str) => JsSuccess(NormalCar(str))
        case _ => JsError(Seq(JsPath() \ "model" -> Seq(ValidationError("error.expected.jsstring"))))
        }
      case None => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.car"))))
    }
    case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.invalid"))))
  }
}

Dialogue & Discussion