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 :)!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.