Michał Jurczuk
Michał Jurczuk

Reputation: 3828

Play framework scala json validation exception

I'm trying to check JsValue object in my Actor using play framework 2.2.2. When I try to use validate method, I receive exception not a result object:

try {
      val result = data.validate[EventConfig]
      Logger.debug("Result: "+result")
    } catch {
        case e =>
           Logger.error("Exception: "+e)
    }

Here is this exception:

Exception: play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(error.expected.jsnumber,WrappedArray())))))

Why is this happening, and how should I use validate method?

====== Update

I was using such Reads implementation:

implicit val EventConfig_reads = new Reads[EventConfig] {
    def reads(json: JsValue): JsResult[EventConfig] = {
        JsSuccess(new
            EventConfig((json \ ConfigEventAttrs.PARAM).as[Int],
              (json \ ConfigEventAttrs.PERIOD).as[Int],
              (json \ ConfigEventAttrs.THRESHOLD).as[Int],
              (json \ ConfigEventAttrs.TOGGLE).as[Boolean]))
    }
  }

The solution is to add catch clause:

implicit val EventConfig_reads = new Reads[EventConfig] {
    def reads(json: JsValue): JsResult[EventConfig] = {
      try {
        JsSuccess(new
            EventConfig((json \ ConfigEventAttrs.PARAM).as[Int],
              (json \ ConfigEventAttrs.PERIOD).as[Int],
              (json \ ConfigEventAttrs.THRESHOLD).as[Int],
              (json \ ConfigEventAttrs.TOGGLE).as[Boolean]))
      } catch {
        case e: JsResultException =>
          JsError(e.errors)
      }
    }
  }

Upvotes: 4

Views: 4256

Answers (2)

Michael Zajac
Michael Zajac

Reputation: 55569

That is not the proper way to use validate. I don't think the documentation highlights it's importance as much as it should, but it's explained here, in the section called Using Validation.

data.validate[EventConfig] returns JsResult and not EventConfig. The preferred way to deal with errors is to fold the result:

data.validate[EventConfig].fold(
   error => {
       // There were validation errors, handle them here.
   },
   config => {
       // `EventConfig` has validated, and is now in the scope as `config`, proceed as usual.
   }
)

Let's examine this a bit. The signature if fold on a JsResult is as follows:

fold[X](invalid: (Seq[(JsPath, Seq[ValidationError])]) ⇒ X, valid: (A) ⇒ X): X

It accepts two functions as arguments that both return the same type of result. The first function is a Seq[(JsPath, Seq[ValidationError])]) => X. In my code above, error has the type Seq[(JsPath, Seq[ValidationError])]), which is essentially just a sequence of json paths tupled with their validation errors. Here you can dissect these errors and return the appropriate error messages accordingly, or do whatever else you may need to on failure.

The second function maps A => X, where A is the type JsResult has been validated as, in your case EventConfig. Here, you'll be able to handle your EventConfig type directly.

Causing and catching exceptions is not the way to handle this (and rarely is), as you will lose all of the accumulated validation errors.


Edit: Since the OP has updated his question with additional information regarding his defined Reads.

The problem with the Reads defined there is that they're using as[T]. When calling as, you're trying to force the given json path to type T, which will throw an exception if it cannot. So as soon as you reach the first validation error, an exception is thrown and you will lose all subsequent errors. Your use case is relatively simple though, so I think it would be better to adopt a more modern looking Reads.

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

case class EventConfig(param: Int, period: Int, threshold: Int, toggle: Boolean)

object EventConfig {

    implicit val jsonReads: Reads[EventConfig] = (
        (__ \ ConfigEventAttrs.PARAM).read[Int] and 
        (__ \ ConfigEventAttrs.PERIOD).read[Int] and 
        (__ \ ConfigEventAttrs.THRESHOLD).read[Int] and 
        (__ \ ConfigEventAttrs.TOGGLE).read[Boolean]
    )(EventConfig.apply _)

}

This is much more compact, and the use of the functional syntax will accumulate all of the validation errors into the JsResult, as opposed to throwing exceptions.


Edit 2: To address the OP's need for a different apply method.

If the parameters you're using the build an object from JSON differ from those of your case class, define a function to use for the JSON Reads instead of EventConfig.apply. Supposing your EventConfig is really like this in JSON:

(time: Long, param: Int)    

But instead you want it to be like this:

case class EventConfig(time: Date, param: Int)

Define a function to create an EventConfig from the original parameters:

def buildConfig(time: Long, param: Int) = EventConfig(DateUtils.timeSecToDate(time), param)

Then use buildConfig instead of EventConfig.apply in your Reads:

implicit val jsonReads: Reads[EventConfig] = (
    (__ \ "time").read[Long] and 
    (__ \ "param").read[Int]
)(buildConfig _)

I shortened this example, but buildConfig can be any function that returns EventConfig and parameters match those of the JSON object you're trying to validate.

Upvotes: 17

Michał Jurczuk
Michał Jurczuk

Reputation: 3828

Validating depends on your Reads method, and I've had an issue there. I should just catch this exception in my reads.

Upvotes: 0

Related Questions