JSON Schema Validation with Play

JSON Schema Validation with Play

In this post, we introduce a way of validating JSON HTTP requests based on a given JSON Schema instead of manually implementing the validation. We were recently approached to implement validation of JSON HTTP requests based on Play’s Validation API and a JSON schema. Play already provides a great API for performing JSON validation via its Reads/Writes combinators, which are also used to convert JSON to other data types. Let’s say, for example, that we model a blog application and we have a Post case:

case class Post(id: Option[Long], title: String, body: String = "")

To ensure that the title of a Post is at least three characters long, we’d create this Reads instance:

val titleIsAtLeast3CharsLong: Reads[Post] = 
  (JsPath \ "title").read[String](Reads.minLength[String](3))
val json = Json.obj("title" -> "Hello there")
json.validate(titleIsAtLeast3CharsLong) // valid

Alternatively, there’s also the Unified Validation library, which is not part of Play itself, but aims to unify the validation concepts across different domains, such as JSON and forms, by writing so-called Rules. The above example rewritten with the Unified Validation library would look as follows:

val titleIsAtLeast3CharsLong = 
  (Path \ "title").from[JsValue](Rules.minLength(3))
val json = Json.obj("title" -> "Oh")
titleIsAtLeast3CharsLong.validate(json) // invalid

Unfortunately, none of these libraries allowed us to use a JSON schema. Since the JSON schema may regularly change and one would need to update all validation rules accordingly, we didn’t want to rewrite the schema via Reads/Writes or Rules either.

Therefore we decided to roll out our own JSON Schema Validator based on the Unified validation library and on Play’s existing validation mechanism.

The basic idea is simple: Instead of wiring the Validation rules using Reads or Rules in our application logic, we’d rather provide a Schema Validator that takes a JSON schema and a JSON instance as input and validates the JSON instance against the schema. In our case, Reads therefore only acts as a mean to convert JSON instances into domain types, but do not contain any validation logic. This goes hand in hand with JSON Macro inception, which allows us to automatically generate Reads/Writes based on a given case class.

In order to illustrate things, here’s an example.

val postReads = Json.reads[Post]
val schema = Json.fromJson[SchemaType](
  Json.parse(
    """{
      |"properties": {
      |  "id":    { "type": "integer" },
      |  "title": { "type": "string", "minLength": 3 },
      |  "body":  { "type": "string" },
      |  "required": ["title"]
      |}
    |}""".stripMargin
  )
).get

The schema requires the title property to be set on an instance with a minimum length of three characters.

In order to demonstrate the typical usage of the Validator, what follows is a snippet from a Play Controller that enforces the submission of valid Posts and returns a BadRequest on invalid ones (e.g. posts featuring titles that do not have at least three characters).

def save = Action(parse.json) { request =>
  val json: JsValue = request.body
  val result: VA[Post] = SchemaValidator.validate(schema, json, postReads)
  // fold applies `invalid` if result is a Failure or `valid` if it is a Success
  result.fold(
      invalid = { errors =>  BadRequest(errors.toJson) },
      valid = { post =>
          Post.save(post)
          Ok(Json.toJson(post))
      }
  )  
}

As you can see we only need to call the SchemaValidator’s validate method. validate actually returns a JsValue, but if we provide it a Reads[A] instance (postReads of type Reads[Post] in this example), it will use that instance to convert the JsValue into an A type, which is Post in this example.

VA is part of the Unified Validation library and represents the possible outcomes: Either a Success or a Failure. In the latter case it will hold the error message why validation has failed.
Of course this little example may seem a bit artificial, but it already illustrates some benefits:

  • We can consume an existing JSON schema instead of rewriting it within the libraries provided in Play
  • It’s more maintainable: especially very complex validation rules tend to get messy and unreadable whereas JSON schema is simple to read and maintain
  • We can easily update it: this is especially true, if the JSON schema is consumed from an external source (e.g., another web service), since we do not need to re-compile and hence could change the validation semantics at runtime
  • If you decide to inline the schema, like we did in the example, the library also comes with Writes that you can utilize to generate a valid JSON schema which then may be consumed by other parts of your tool chain

If you would like to try this out yourself head over to this github repo. We would be very happy to hear any feedback, so get in touch with us!

 

Guest Blog Post
Guest Author: Edgar Müller