1

I have json rest api application based on play framework and want to receive information about validation errors when I parse incoming requests. Everything is working fine except conversion from json array to json value. Json structure I want to achieve:

{
  "errors": {
    "name": ["invalid", "tooshort"],
    "email": ["invalid"]
  }
}

When I tried to implement a sample it worked perfectly:

def error = Action {
  BadRequest(obj(
    "errors" -> obj(
      "name" -> arr("invalid", "tooshort"),
      "email" -> arr("invalid")
    )
  ))
}

When I tried to extract the changing part like this:

def error = Action {
  val e = Seq("name" -> arr("invalid", "tooshort"), "email" -> arr("invalid"))
  // in real app it will be Seq[(JsPath, Seq[ValidationError])] 
  BadRequest(obj(
    "errors" -> obj(e: _*)
  ))
}

I got a compiler error:

type mismatch; found : Seq[(String, play.api.libs.json.JsArray)] required: Seq[(String, play.api.libs.json.Json.JsValueWrapper)]

Maybe there is some implicit conversion I'm missing from JsArray to JsValueWrapper? But then, why does the sample works fine in the same file with the same imports?

Play 2.1.1, Scala 2.10.0

UPDATE: Problem resolved thanks to Julien Lafont, the final code:

implicit val errorsWrites = new Writes[Seq[(JsPath, Seq[ValidationError])]] {
  /**
   * Maps validation result of Ember.js json request to json validation object, which Ember can understand and parse as DS.Model 'errors' field.
  *
  * @param errors errors collected after json validation
  * @return json in following format:
  *
  * {
  *   "errors": {
  *     "error1": ["message1", "message2", ...],
  *     "error2": ["message3", "message4", ...],
  *     ...
  *   }
  * }
  */
 def writes(errors: Seq[(JsPath, Seq[ValidationError])]) = {
   val mappedErrors = errors.map {
     e =>
       val fieldName = e._1.toString().split("/").last // take only last part of the path, which contains real field name
       val messages = e._2.map(_.message)
       fieldName -> messages
   }

   obj("errors" -> toJson(mappedErrors.toMap)) // Ember requires root "errors" object
  }
}

Usage:

def create = Action(parse.json) { // method in play controller
  request =>
    fromJson(request.body) match {
      case JsSuccess(pet, path) => Ok(obj("pet" -> Pets.create(pet)))
      case JsError(errors) => UnprocessableEntity(toJson(errors)) // Ember.js expects 422 error code in case of errors
    }
}

2 Answers 2

4

You can simply define your errors in a Map[String, Seq[String]], and transform it in Json with Json.toJson (there are built-in writers for Map[String,Y] and Seq[X])

scala> val e = Map("name" -> Seq("invalid", "tooshort"), "email" -> Seq("invalid"))
e: scala.collection.immutable.Map[String,Seq[String]] = Map(name -> List(invalid, tooshort), email -> List(invalid))

scala> Json.toJson(e)
res0: play.api.libs.json.JsValue = {"name":["invalid","tooshort"],"email":["invalid"]}

scala> Json.obj("errors" -> Json.toJson(e))
res1: play.api.libs.json.JsObject = {"errors":{"name":["invalid","tooshort"],"email":["invalid"]}}
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the solution! Converting result of json validation to map of strings appeared to be the simplest way to resolve this problem. Updated question with the last version of code.
Unfortunately, this rather ugly and inefficient call mappedErrors.toMap on Seq of strings can't be removed because scala json doesn't have implicit writes for Seq[(String, Seq[String])].
2

The reason the long handed version works is because scala's automatic type inference triggers an implicit conversion toJsFieldJsValueWrapper.

For example

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

scala> import play.api.libs.functional.syntax._
import play.api.libs.functional.syntax._

scala> import Json._
import Json._

scala> arr("invalid", "tooshort")
res0: play.api.libs.json.JsArray = ["invalid","tooshort"]

scala> obj("name" -> arr("invalid", "tooshort"))
res1: play.api.libs.json.JsObject = {"name":["invalid","tooshort"]}

scala> obj _
res3: Seq[(String, play.api.libs.json.Json.JsValueWrapper)] => play.api.libs.json.JsObject = <function1>

Notice that arr returns a JsArray, however obj requires a JsValueWrapper. Scala is able to convert the JsArray to JsValueWrapper when it constructs the arguments for obj. However it is not able to convert a Seq[(String, JsArray)] to a `Seq[(String, JsValueWrapper)].

If you provide the expected type when of the sequence in advance, the Scala compiler's type inferencer will perform the conversion as it creates the sequence.

scala> Seq[(String, JsValueWrapper)]("name" -> arr("invalid", "tooshort"), "email" -> arr("invalid"))
res4: Seq[(String, play.api.libs.json.Json.JsValueWrapper)] = List((name,JsValueWrapperImpl(["invalid","tooshort"])), (email,JsValueWrapperImpl(["invalid"])))

However once the sequence is created it cannot be converted unless there is an implicit conversion in scope that can convert sequences.

The final code snippet looks like:

def error = Action {
  val e = Seq[(String, JsValueWrapper)]("name" -> arr("invalid", "tooshort"), "email" -> arr("invalid"))
  // in real app it will be Seq[(JsPath, Seq[ValidationError])] 
  BadRequest(obj(
    "errors" -> obj(e: _*)
  ))
}

1 Comment

Thanks for an explanation! Implicit conversion to JsValueWrapper played a trick with my mind at first.

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.