Json "Validate" for Play

Viewed 3860

For the validate method on request.body it matches the attribute name and value type of the json object to those defined in the model definition. Now if I were to add an extra attribute to the json object and try to validate it, it passes as a JsSuccess when it shouldn't.

{ 
    "Name": "Bob",
    "Age": 20,
    "Random_Field_Not_Defined_in_Models": "Test"
}

My Person Class is defined as follows

case class Person(name: String, age: Int)
2 Answers

If you want to avoid too much boilerplate it is possible to make a more generic solution using a little bit of scala reflection:

import play.api.libs.json._
import scala.reflect.runtime.universe._

def checkedReads[T](underlyingReads: Reads[T])(implicit typeTag: TypeTag[T]): Reads[T] = new Reads[T] {

    def classFields[T: TypeTag]: Set[String] = typeOf[T].members.collect {
      case m: MethodSymbol if m.isCaseAccessor => m.name.decodedName.toString
    }.toSet

    def reads(json: JsValue): JsResult[T] = {
      val caseClassFields = classFields[T]
      json match {
        case JsObject(fields) if (fields.keySet -- caseClassFields).nonEmpty =>
          JsError(s"Unexpected fields provided: ${(fields.keySet -- caseClassFields).mkString(", ")}")
        case _ => underlyingReads.reads(json)
      }
    }

  }

Then you can specify your reads instances as:

implicit val reads = checkedReads(Json.reads[Person])

This leverages a fair bit of Scala type magic and also the reflection library (that lets you look at fields on classes).

Rather than relying on a fixed set of fields the classFields method gets all of the fields dynamically for the case class (type param T). It looks at all of the members and collects only the case class accessors (otherwise we'd pick up methods like toString). It returns a Set[String] of field names.

You'll notice that the checkedReads takes an implicit TypeTag[T]. This is supplied by the compiler at compile time and used by the typeOf method.

The remaining code is fairly self explanatory. If the incoming json matches our first case (it is a JsObject and there are fields not on the case class) then we return a JsError. Otherwise we pass it on to the underlying reader.

Related