I'm trying to learn Scala/Play so I created a sample api that uses WSRequest to connect to GitHub and returns some info based on the user's id. I can convert this response to JsValue by doing:
val response: JsValeu = result.json
Ok(json)
I am having trouble when trying to manipulate the JsValue, for example filter values based on some criteria, etc? Do I need to convert it to a JsObject? I've looked on the Play documentation but I can't figure out how to do this.
What is the approach when handling JsValue?
Thanks
JsValue signifies any sort of JSON data entity, including objects, numbers, strings, etc.
If you want to filter values in a JsObject, then you'll have to "cast" your JsValue into a JsObject. like:
val jsonObject: JsObject = response.as[JsObject]
Then you can mutate the object how you like.
Read the documentation on JsObject and JsValue to find out how to do the rest of what you're trying to do.
https://www.playframework.com/documentation/2.5.x/api/scala/index.html#play.api.libs.json.JsObject
https://www.playframework.com/documentation/2.5.x/api/scala/index.html#play.api.libs.json.JsValue
Related
I'm still pretty new to Scala and arrived at some kind of typing-roadblock.
Non-SQL databases such as mongo and rethinkdb do not enforce any scheme for their tables and manage data in json format. I've been struggling to get the java API for rethinkdb to work on Scala and there seems to be surprisingly low information on how to actually use the results returned from the database.
Assuming a simple document schema such as this:
{
"name": "melvin",
"age": 42,
"tags": ["solution"]
}
I fail to get how to actually this data in Scala. After running a query, for example, by running something like r.table("test").run(connection), I receive an object from which I can iterate AnyRef objects. In the python word, this most likely would be a simple dict. How do I convey the structure of this data to Scala, so I can use it in code (e.g., query fields of the returned documents)?
From a quick scan of the docs and code, the Java Rethink client uses Jackson to handle deserialization of the JSON received from the DB into JVM objects. Since by definition every JSON object received is going to be deserializable into a JSON AST (Abstract Syntax Tree: a representation in plain Scala objects of the structure of a JSON document), you could implement a custom Jackson ObjectMapper which, instead of doing the usual Jackson magic with reflection, always deserializes into the JSON AST.
For example, Play JSON defers the actual serialization/deserialization to/from JSON to Jackson: it installs a module into a vanilla ObjectMapper which specially takes care of instances of JsValue, which is the root type of Play JSON's AST. Then something like this should work:
import com.fasterxml.jackson.databind.ObjectMapper
import play.api.libs.json.jackson.PlayJsonModule
// Use Play JSON's ObjectMapper... best to do this before connecting
RethinkDB.setResultMapper(new ObjectMapper().registerModule(new PlayJsonModule(JsonParserSettings())))
run(connection) returns a Result[AnyRef] in Scala notation. There's an alternative version, run(connection, typeRef), where the second argument specifies a result type; this is passed to the ObjectMapper to ensure that every document will either fail to deserialize or be an instance of that result type:
import play.api.libs.json.JsValue
val result = r.table("table").run(connection, classOf[JsValue]) : Result[JsValue]
You can then get the next element from the result as a JsValue and use the usual Play JSON machinery to convert the JsValue into your domain type:
import play.api.libs.json.Json
case class MyDocument(name: String, age: Int, tags: Seq[String])
object MyDocument {
implicit val jsonFormat = Json.format[MyDocument]
}
// result is a Result[JsValue] ... may need an import MyDocument.jsonFormat or similar
val myDoc = Json.fromJson[MyDocument](result.next()).asOpt[MyDocument] : Option[MyDocument]
There's some ability with enrichments to improve the Scala API to make a lot of this machinery more transparent.
You could do similar things with the other Scala JSON ASTs (e.g. Circe, json4s), but might have to implement functionality similar to what Play does with the ObjectMapper yourself.
I have a simple case class:
case class Account(accountId: Long, login: DateTime)
Now I want to retrieve docs from Couchbase bucket by simple N1QL query (it should return a simple list of JSON documents contain two fields):
val query = "SELECT u.accountId, u.login FROM `accounts` u WHERE DATE_DIFF_STR(NOW_STR(), u.login, 'day') > 30"
bucket.query(N1qlQuery.simple(query)).map(rows => rows.map(row => row.value().asInstanceOf[Account]).seq)
but, I got an error there in postman:
java.lang.ClassCastException: com.couchbase.client.java.document.json.JsonObject cannot be cast to com.package.account
My question is - how I could cast docs from database into my, custom object? I also tried to cast it into RawJSONDocument first, but it did not help.
Can someone help me with that?
First, you may be interested to know that we're actively working on a native Couchbase Scala SDK at present for release this year, and it will support your use case of converting rows directly into a case class.
But in the here-and-now, no you cannot directly cast a JsonObject into a case class. You will need to use toString to pull out the raw JSON string, and then use a Scala JSON library to convert it. You've got several options here:
Jackson
val json = row.value().toString()
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val account = mapper.readValue(json, classOf[Account])
uPickle
val account = upickle.default.read[Account](json)
Jsoniter
val account = com.github.plokhotnyuk.jsoniter_scala.core.readFromString[Account](json)
Plus there's Circe, Json4s, Play Json, Jawn etc..
FWIW, I found Jsoniter to be the fastest in my benchmarking.
I need to parse several json fields, which I'm using Play Json to do it. As parsing may fail, I need to throw a custom exception for each field.
To read a field, I use this:
val fieldData = parseField[String](json \ fieldName, "fieldName")
My parseField function:
def parseField[T](result: JsLookupResult, fieldName: String): T = {
result.asOpt[T].getOrElse(throw new IllegalArgumentException(s"""Can't access $fieldName."""))
}
However, I get an error that reads:
Error:(17, 17) No Json deserializer found for type T. Try to implement
an implicit Reads or Format for this type.
result.asOpt[T].getOrElse(throw new IllegalArgumentException(s"""Can't access $fieldName."""))
Is there a way to tell the asOpt[] to use the type in T?
I strongly suggest that you do not throw exceptions. The Play JSON API has both a JsSuccess and JsError types that will help you encode parsing errors.
As per the documentation
To convert a Scala object to and from JSON, we use Json.toJson[T: Writes] and Json.fromJson[T: Reads] respectively. Play JSON provides the Reads and Writes typeclasses to define how to read or write specific types. You can get these either by using Play's automatic JSON macros, or by manually defining them. You can also read JSON from a JsValue using validate, as and asOpt methods. Generally it's preferable to use validate since it returns a JsResult which may contain an error if the JSON is malformed.
See https://github.com/playframework/play-json#reading-and-writing-objects
There is also a good example on the Play Discourse forum on how the API manifests in practice.
I'm trying to support arbitrary filters for a REST API that fetches a list of documents from MongoDB.
For instance
//example.com/users <- list all
//example.com/users?age=30 <- all users who are 30
//example.com/users?age=30&name=John <- all users who are 30 and called John
...
I'm using Play-ReactiveMongo and dealing with JSONCollection objects only.
So in my routes I put
GET /users controllers.Users.list(id: Option[String], name: Option[String], age: Option[Int])
But there are two problem with that, first I'll need to have a pretty long list of optional parameters, and then in my controller I need to use pattern matching on all of them to check whether they're empty or not, and also build the selector that I use to filter my collection.
var filters = JsObject(Nil)
name match {
case Some(x) => filters += ("name" -> JsString(x))
case None => None
}
I realized that I can get the full query string from the request object, which is a Map[String, Seq[String]]. But then I don't know a good way to check whether values are String or something else.
Is there another better and idiomatic way to do what I want?
Possible solution can be:
Use POST instead of GET:
POST /example.com/users
"data"={"age":25, "name":"xyz", ... }
OR single param in GET:
GET /example.com/users?filter={"age":25, "name":"xyz", ... }
On the server side, just validate against your model class OR just pass the same json in your reactivemongo find method.
Maybe request binders will help you to create complex objects from varying request parameters.
https://www.playframework.com/documentation/2.5.x/ScalaRequestBinders#QueryStringBindable
For example, you could build something like this (from the docs):
case class AgeRange(from: Int, to: Int)
for requests like this:
/age?from=1&to=10
Now you could change these attributes to Option and create a function that creates a reactivemongo query based on the values at hand.
I'm exploring the Play Framework and have run in to a bit of a corner.
After making an API call to Google Analytics with the WS library, I receive a Future[Response] object. After digesting that Response object, I get the data I really care about, but because it's wrapped in a Future, I'm having some trouble writing it to the browser.
OK( gaApiString )
This gives me an error that reads:
Cannot write an instance of scala.concurrent.Future[String] to HTTP
response. Try to define a Writeable[scala.concurrent.Future[String]]
I'm having some trouble finding & understanding how to use a Writable object. Little help?
You need to map the Future to a Future[Result], passing it to Action.async.
def test = Action.async {
val wsResult: Future[String] = ...
wsResult.map { gaApiString =>
Ok(gaApiString)
}
}
If gaApiString is actually List[String], then it depends on what you want to do with it. Displaying it as a comma-separated list you could just change it to Ok(gaApiString.mkString(",")). The key here is mapping the Future to manipulate the value after it's completed.