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?
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 _)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With