Search code examples
jsonscalaplayframeworkplayframework-json

How to avoid automatic casting provided by Play Framwork Json Reads but get an exception instead


Reads appears to do auto cast for me if the casting is possible. e.g., Float -> Int. For example, if the code gets json as following,

{
"name": "Jack",
"age": 22.4,
"role": "Coder"
}

the instance of class Person would have field age of 22 instead of getting an invalid argument exception. If I do want an exception in that case, what's the best solution? thx a lot.

case class Person(val name: String, val age: Int, val role: String)

object Person {
implicit val residentReads: Reads[Resident] = (
  (JsPath \ "name").read[String](minLength[String](3)) and
  (JsPath \ "age").read[Int](min(0)) and
  (JsPath \ "role").readNullable[String]
)(Resident.apply _)

...
}

Solution

  • Looking at the source code, we can see why. Any value matching a JsNumber has toInt called upon it:

    implicit object IntReads extends Reads[Int] {
        def reads(json: JsValue) = json match {
            case JsNumber(n) => JsSuccess(n.toInt)
            case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsnumber"))))
        }
    }
    

    To avoid this, we can implement a new Reads[Int] that will not validate non-integers:

    import play.api.libs.json._
    import play.api.libs.json.Reads._
    import play.api.libs.functional.syntax._
    import play.api.data.validation._
    
    implicit object WholeIntReads extends Reads[Int] {
        def reads(json: JsValue) = json match {
            case JsNumber(n) if(n.isValidInt) => JsSuccess(n.toInt)
            case _ => JsError(Seq(JsPath() -> Seq(ValidationError("error.expected.jsnumber"))))
        }
    }
    

    And use it like this:

    case class Person(val name: String, val age: Int, val role: String)
    
    object Person {
        implicit val residentReads: Reads[Person] = (
            (JsPath \ "name").read[String](minLength[String](3)) and
            (JsPath \ "age").read[Int](WholeIntReads keepAnd min(0)) and
            (JsPath \ "role").read[String]
        )(Person.apply _)
    }
    

    (There were some inconsistencies in your sample code, so I fixed them to make this compile. The only relevant line is age.)

    Results:

    scala> Json.parse("""{"name": "Jack","age": 22.4,"role": "Coder"}""").validate[Person]
    res13: play.api.libs.json.JsResult[Person] = JsError(List((/age,List(ValidationError(error.expected.jsnumber,WrappedArray())))))
    
    scala> Json.parse("""{"name": "Jack","age": 22,"role": "Coder"}""").validate[Person]
    res14: play.api.libs.json.JsResult[Person] = JsSuccess(Person(Jack,22,Coder),)
    
    scala> Json.parse("""{"name": "Jack","age": -22,"role": "Coder"}""").validate[Person]
    res15: play.api.libs.json.JsResult[Person] = JsError(List((/age,List(ValidationError(error.min,WrappedArray(0))))))
    

    Note that I'm using validate[T] and not throwing an exceptions, as this is best practice. This will allow the ValidationErrors to accumulate and be handled without needing any exceptions.