0

I've been trying several things now to get this API to work, but somehow it just won't. The array of logs will not convert to List[Log]. I'm using reactivemongo and the play framework for Scala.

Via a web platform, the following json is arriving at the API:

{ 
  name: 'name', 
  logs: [{timestamp: 1, activity: 'something'}]
}

Now I've got two models, one for each of the Objects:

import play.api.libs.json.Json
  case class User(name: String, logs: List[Log])
  object User { implicit val userFormatter = Json.format[User]

import play.api.libs.json.Json
  case class Log(timestamp: String, activity: String)
  object Log { implicit val logFormatter = Json.format[Log]

In the controller, I'm trying to read the JsObject as follows:

@Singleton
class UserController @Inject()(val reactiveMongoApi: ReactiveMongoApi)(implicit exec: ExecutionContext) extends Controller with MongoController with ReactiveMongoComponents {


  val transformer: Reads[JsObject] =
      Reads.jsPickBranch[JsString](__ \ "name") and
      Reads.jsPickBranch[JsArray](__ \ "logs") reduce

  //get the user collection from db
  def persons: JSONCollection = db.collection[JSONCollection]("user")

  var clazz: Class[_ <: JsObject] = _

  //create a new user entry
  def create(name: String,
         logs: List[Log]) = Action.async {

val json = Json.obj(
  "name" -> name,
  "logs" -> logs)

persons.insert(json).map(lastError =>
  Ok("Mongo LastError: %s".format(lastError)))

}

The route is as follows: POST /user/add controllers.UserController.create(name: String, logs: List[models.Log])

It tells me to [error] /ARest-api/conf/routes:26: No QueryString binder found for type List[models.Log]. Try to implement an implicit QueryStringBindable for this type.

Which I have tried. I replaced the logFormatter line of code with the following:

implicit def LogBinder(implicit stringBinder: QueryStringBindable[String]) =
new QueryStringBindable[Log] {

  override def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Log]] = {
    Some({
      val timestamp = stringBinder.bind(key + ".timestamp", params)
      val activity = stringBinder.bind(key + ".activity", params)
      (timestamp, activity) match {
        case (Some(Right(timestamp)), Some(Right(activity))) => Right(Log(timestamp, activity))
        case _ => Left("Unable to bind Log")
      }
    })
  }

  override def unbind(key: String, log: Log): String =
    stringBinder.unbind(
      key + ".timestamp", log.timestamp) +
      "&" + stringBinder.unbind(key + ".activity", log.activity)
}

Unfortunately this does not work. I've been struggling with this for several days now, and couldn't get it to work. Could someone please tell me if there perhaps is a better way to do this, or help me fix arrayreading?

Thanks in advance!

1 Answer 1

2

Not the most elegant solution but this is atleast one way to make it work.

To get the json sent in the post request you can do the following:

  • Change the route to simply POST /user/add controllers.UserController.create as you will get the user info in the Post body.

  • Get the Post message in your Action by adding this to it:

    def create = Action.async(BodyParsers.parse.json) { request =>
    

    this will use a JSON specific BodyParser to parse the request and provide request.body as a JsValue.

Now we can parse the received json using case classes:

case class User(name: String, logs: List[Log])
case class Log(timestamp: String, activity: String)

and Reads objects:

implicit val logReads = Json.reads[Log]
implicit val userReads = (
  (JsPath \ "name").read[String] and
  (JsPath \ "logs").read[List[Log]]
)(User.apply _)

Finally the parsing:

val user: Option[User] = Json.fromJson[User](request.body).asOpt


The final code would look something like this:

@Singleton
class HomeController @Inject() extends Controller {
  implicit val logReads = Json.reads[Log]
  implicit val userReads = (
    (JsPath \ "name").read[String] and
    (JsPath \ "logs").read[List[Log]]
  )(User.apply _)

  def create = Action.async(BodyParsers.parse.json) { request =>

    val user: Option[User] = Json.fromJson[User](request.body).asOpt
    Future(Ok)
  }

}

case class User(name: String, logs: List[Log])
case class Log(timestamp: String, activity: String)

Hopefully this will help solving your problem!

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

1 Comment

Thanks for the answer! I will definitely try this tomorrow, and let you know if it solved the problem :)!

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.