Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom JSON validation constraints in Play Framework 2.3 (Scala)

I managed to implement form validation with custom constraints, but now I want to do the same thing with JSON data.

How can I apply custom validation rules to a JSON parser?

Example: The client's POST request contains a user name (username) and not only do I want to make sure that this parameter is a non-empty text, but also that this user actually exists in the database.

// In the controller...

def postNew = Action { implicit request =>
    request.body.asJson.map { json =>
        json.validate[ExampleCaseClass] match {
            case success: JsSuccess[ExampleCaseClass] =>
                val obj: ExampleCaseClass = success.get
                // ...do something with obj...
                Ok("ok")
            case error: JsError =>
                BadRequest(JsError.toFlatJson(error))
        }
    } getOrElse(BadRequest(Json.obj("msg" -> "JSON request expected")))
}


// In ExampleCaseClass.scala...

case class ExampleCaseClass(username: String, somethingElse: String)

object ExampleCaseClass {
    // That's what I would use for a form:
    val userCheck: Mapping[String] = nonEmptyText.verifying(userExistsConstraint)

    implicit val exampleReads: Reads[ExampleCaseClass] = (
        (JsPath \ "username").read[String] and
        (JsPath \ "somethingElse").read[String]
    )(ExampleCaseClass.apply _)
}

That's as far as I get, but this only ensures that username is a String. How do I apply my additional custom validation rule, e.g. to check if the given user really exists? Is this even possible?

Sure, I could take my obj in the case success section in the action and perform additional checks there, but this doesn't seem very elegant, because then I'd have to create my own error message and could only user JsError.toFlatJson(error) for some cases. After searching and trying for hours I couldn't find any examples.

For regular forms I'd use something like this:

// In the controller object...

val userValidConstraint: Constraint[String] = Constraint("constraints.uservalid")({ username =>
    if (User.find(username).isDefined) {
        Valid
    } else {
        val errors = Seq(ValidationError("User does not exist"))
        Invalid(errors)
    }
})

val userCheck: Mapping[String] = nonEmptyText.verifying(userValidConstraint)

val exampleForm = Form(
    mapping(
        "username" -> userCheck
        // ...and maybe some more fields...
    )(ExampleCaseClass.apply)(ExampleCaseClass.unapply)
)


// In the controller's action method...

exampleForm.bindFromRequest.fold(
    formWithErrors => {
        BadRequest("Example error message")
    },
    formData => {
        // do something
        Ok("Valid!")
    }
)

But what if the data is submitted as JSON?

like image 546
Nick Avatar asked Oct 11 '14 17:10

Nick


1 Answers

The simplest way I can think of would use the filter method from Reads.

Let's say we have some User object that will determine if the user name exists:

object User {
    def findByName(name: String): Option[User] = ...
}

You could then construct your Reads like this:

import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.data.validation._

case class ExampleCaseClass(username: String, somethingElse: String)

object ExampleCaseClass {
    implicit val exampleReads: Reads[ExampleCaseClass] = (
        (JsPath \ "username").read[String].filter(ValidationError("User does not exist."))(findByName(_).isDefined) and
        (JsPath \ "somethingElse").read[String]
    )(ExampleCaseClass.apply _)
}

Your controller function can be simplified using a json BodyParser and fold:

def postNew = Action(parse.json) { implicit request =>
    request.body.validate[ExampleCaseClass].fold(
        error => BadRequest(JsError.toFlatJson(error)),
        obj => {
            // Do something with the validated object..
        }
    )
}

You could also create a separate Reads[String] that will check if the user exists, and explicitly use that Reads[String] within your Reads[ExampleCaseClass]:

val userValidate = Reads.StringReads.filter(ValidationError("User does not exist."))(findByName(_).isDefined)

implicit val exampleReads: Reads[ExampleCaseClass] = (
    (JsPath \ "username").read[String](userValidate) and
    (JsPath \ "somethingElse").read[String]
)(ExampleCaseClass.apply _)
like image 170
Michael Zajac Avatar answered Nov 09 '22 01:11

Michael Zajac