0
votes

It is straightforward to read the expected fields (no matter optional or not) and validate them, but I fail on how to throw an exception properly if an unexpected field is detected in the given json. It'd be great if the play framework could help on it with one or two statements. Yes, I can process the json and get all the fields' names and compare them to the expected list, which appears to be a bit complicated (when the json input's format is complicated).

For example, the input json is expected as following:

{
  "param": 1,
  "period": 2,  
  "threshold": 3,
  "toggle": true
}

and the scala code to fultill the class EventConfig from the json input:

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] = (
    (__ \ "param").read[Int] and 
    (__ \ "period").read[Int] and 
    (__ \ "threshold").read[Int] and 
    (__ \ "toggle").read[Boolean]
  )(EventConfig.apply _)
}

I'd like to have an exception thrown if an unexpected filed is detected in the json such as

{
  "param": 1,
  "period": 2,  
  "threshold": 3,
  "toggle": true,
  "foo": "bar"
}
2
Throwing an exception in scala is discouraged, first of all. But I wonder, if the JSON contains all of the fields it's looking for and validates them, but contains one field it does not expect, why should that throw an error? Currently, your Reads would just ignore the foo field.Michael Zajac
Thanks @LimbSoup. In the real project the json file is different and from external customers/third party. We'd like to offer better error notification if customers make some typo and fail it at the very beginning instead of submitting the request to the backend system.Dan

2 Answers

2
votes

Have you checked out validate?

I don't know how to deep link you on this page but check out https://www.playframework.com/documentation/2.1.3/ScalaJson and search for heading: Safest conversion with validate[T]

A few examples from that page:

scala> import play.api.libs.json._

scala> val jsres: JsResult[String] = JsString("toto").validate[String]
jsres: JsSuccess("toto")

scala> val jsres: JsResult[String] = JsNumber(123).validate[String]
jsres: play.api.libs.json.JsResult[String] =         JsError(List((,List(ValidationError(validate.error.expected.jsstring,WrappedArray())))))

jsres.map{ s: String => …}
jsres.flatMap{ s: String => JsSuccess(s) }

jsres.fold( 
 errors: Seq[(JsPath, Seq[ValidationError])] => // manage errors,
 s: String => // manage value 
)

If I understand your question completely - I think you would be most interested in using validate along with folding on the JsResult so that you can handle success/happy path and then errors(where you can return 400 bad request etc or however you want to handle it).

psuedo code

def something  = Action.async(parse.json) { request =>
 val maybeEventConfig = request.body.validate[EventConfig]
maybeEventConfig.fold(
 error => {},
 eventConfig => {}
 )}
0
votes

I'd like to +1 on what LimbSoup said above about not necessarily needing to throw an error if additional tags are sent your way; after all, if the client has a typo in a field name you're expecting, your Reads will throw an exception anyway, which you can handle to provide the appropriate feedback to your clients.

If you still think you need this, however, you could consider JSON Schema (XSD equiv for JSON): http://json-schema.org/

There's a Java implementation of the validator, which you can of course, use with Scala. https://github.com/fge/json-schema-validator

Once again, I'd like to emphasise that your existing code will already choke on typos of expected fields. Consider if you really need this.

[edit]
I just wanted to reiterate the original point I made about not explicitly validating against a document. The idea is that your code should be tolerant of changes to the data that's received. It should make as few assumptions about the structure of the data as possible. The concept is known as Tolerant Reader. See Martin Fowler's more eloquent explanation.