4

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)
      }
    }
  }

2 Answers 2

17

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.

Sign up to request clarification or add additional context in comments.

6 Comments

In my code I'm using fold method, I wrote in such way for simplicity. And I wanted just to print result, not an EventConfig. However I'm still getting this exception. The problem was with bad Reads implementations, and it throws JsResultException. Event if you will use fold, it still throws this exception, I will add this Reads implementation in update. Thank you for explanation.
@JMichal I've updated my answer to address the Reads.
In my "real" code I had to use additional parameters in apply, that's why I've used such construction, or maybe you know how to add them in this syntax?
What do you mean by "additional parameters" and how would they be different than this?
EventConfig have a parameter time where I'm passing converted time in seconds to milis: DateUtils.timeSecToDate((json \ ConfigEventAttrs.TIME).as[Long])
|
0

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

2 Comments

:- This should have been added as a comment, not an answer
But this is an answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.