With Play/Scala, I try to transform this json:
val json = Json.parse("""
{
"name": "John Doe",
"location": {
"lon": 48.858596,
"lat": 2.294481
}
}
""")
into this result:
val result = Json.parse("""
{
"name": "John Doe",
"location": {
"type": "Point",
"coordinates": [48.858596, 2.294481]
}
}
""")
Any idea how to apply the magic? Here's what i tried:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
val transformer = {
val locationField = __ \ "location"
val lon = (locationField \ "lon").json.pick
val lat = (locationField \ "lat").json.pick
__.json.update((
(locationField \ "type").json.put( JsString("Point") ) and
(locationField \ "coordinates").json.put( JsArray() )).reduce
andThen
(locationField \ "coordinates").json.update(of[JsArray].map { // How to add lon/lat into JsArray?????
case JsArray(arr) => JsArray(arr :+ JsNumber(3L))
}
)
andThen (
(locationField \ "lon").json.prune and
(locationField \ "lat").json.prune).reduce
)
}
json.transform(transformer)
Get the code here: https://gist.github.com/chrissom/20c5aa254210d7c32f53479df6a66f68
Assuming the import:
import play.api.libs.json._
You can define a Location that would represent the location value.
case class Location(lon: Double, lat: Double) {
def toPoint: JsObject = {
Json.obj(
"type" -> "Point",
"coordinates" -> Json.arr(lon, lat)
)
}
}
object Location {
implicit val reads: Reads[Location] = Json.reads[LonLat]
}
Now you could do:
val locationJson = Json.parse("""{"lon": 48.858596, "lat": 2.294481}""")
locationJson.transform(of[LonLat].map(_.toPoint))
// Output:
// JsSuccess({"type":"Point","coordinates":[48.858596,2.294481]},)
Now you can plug this into a transformation such as:
(__ \ 'location).json.update(of[LonLat].map(_.toPoint))
But you'll be left over with lat and lon fields in this transformation. So remove them:
(__ \ 'location).json.update(of[LonLat].map(_.toPoint)) andThen
(__ \ 'location \ 'lat).json.prune andThen
(__ \ 'location \ 'lon).json.prune
It appears to me that it is not possible to simplify this as update is the only operation of its kind - which happens to be a deep-merge.
Related
I'm trying to set up a JSON response that I got from a weather API to fit in a model class I defined in order to use it easily, but I can't get to do it.
Here is the class :
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Forecast(var main: String, var description: String, var temp: Int, var tempMin: Int, var tempMax: Int)
object Forecast {
implicit val forecastJsonFormat: Reads[Forecast] = (
(JsPath \ "weather" \\"main").read[String] and
(JsPath \ "weather" \\"description").read[String] and
(JsPath \ "main" \\"temp").read[Int] and
(JsPath \ "main" \\"temp_min").read[Int] and
(JsPath \ "main" \\"temp_max").read[Int]
) (Forecast.apply _)
}
and this is the code in the controller :
def weather = Action.async {
futureResponse.map(response => {
val jsonString = response.json.toString()
val jsonObject = Json.parse(jsonString)
// TODO: Create t [Forecast] Object which represents the response.json data to send it to the view below
Ok(views.html.weather(t))
})}
example of the response.json I'am getting :
{"coord":{"lon":37.62,"lat":55.75},"weather":[{"id":600,"main":"Snow","description":"light snow","icon":"13n"},{"id":701,"main":"Mist","description":"mist","icon":"50n"}],"base":"stations","main":{"temp":269.15,"pressure":1024,"humidity":92,"temp_min":268.15,"temp_max":270.15},"visibility":3100,"wind":{"speed":2,"deg":200},"clouds":{"all":90},"dt":1546266600,"sys":{"type":1,"id":9029,"message":0.0029,"country":"RU","sunrise":1546235954,"sunset":1546261585},"id":524901,"name":"Moscow","cod":200}
You have to change main to Seq[String] and description to Seq[String] and temp, tempMin, tempMax to Double
I used a different way to create the reads here, but this way will throw an exception if the format is different than the expected format.
case class Forecast(main: Seq[String], description: Seq[String], temp: Double, tempMin: Double, tempMax: Double)
object Forecast {
val reads = new Reads[Forecast] {
override def reads(json: JsValue): JsResult[Forecast] = {
val main = (json \ "weather" \\ "main").map(_.as[String]).toList
val description = (json \ "weather" \\ "description").map(_.as[String]).toList
val temp = (json \ "main" \ "temp").as[Double]
val tempMin = (json \ "main" \ "temp_min").as[Double]
val tempMax = (json \ "main" \ "temp_max").as[Double]
JsSuccess(Forecast(main, description, temp, tempMin, tempMax))
}
}
}
or you can use the same way you are using, but parse the list in different way:
val forecastJsonFormat: Reads[Forecast] = (
(JsPath \ "weather").read[List[Map[String, JsValue]]].map(_.map(_("main").as[String])) and
(JsPath \ "weather").read[List[Map[String, JsValue]]].map(_.map(_("description").as[String])) and
(JsPath \ "main" \ "temp").read[Double] and
(JsPath \ "main" \ "temp_min").read[Double] and
(JsPath \ "main" \ "temp_max").read[Double]
) (Forecast.apply _)
I finally got to do it and here is how :
In the model I have my case class defined and a companion object that parses the JSON response I got from the web API to my class arguments
Model code :
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class Forecast(main: String, description: String, temp: Double, tempMin: Double, tempMax: Double)
object Forecast {
implicit val forecastReads: Reads[Forecast] = (
(JsPath \ "weather" \\ "main").read[String] and
(JsPath \ "weather" \\ "description").read[String] and
(JsPath \ "main" \ "temp").read[Double] and
(JsPath \ "main" \ "temp_min").read[Double] and
(JsPath \ "main" \ "temp_max").read[Double]
) (Forecast.apply _)
}
In the controller code I added a pattern matching and here it is !
Controller code :
def weather = Action.async {
futureResponse.map(response => {
val parseResult = Json.fromJson[Forecast](response.json)
parseResult match {
case JsSuccess(forecast, JsPath) => Ok(views.html.weather(forecast))
case JsError(error) => InternalServerError("Something went wrong!!") // Note that I'm not sure this result exists in Play...
}
})
}
I need to serialize a String and an Option[Boolean]:
val myWrites = (
(__ \ "box").write(
(
(__ \ "name").write[String] ~
(__ \ "default").writeNullable[Boolean]
).tupled
)
)
If Option[Boolean] is Some then I'd expect
{
"box": {
"name": "John",
"default": true
}
}
... while if Option[Boolean] is None I'd expect
{
"box": {
"name": "John"
}
}
Given the following variables...
val name = "John"
val default = Some(true)
... how do I pass them to the Writes? I've tried this:
myWrites.writes(name, defaul)
... but it doesn't compile:
No Json serializer found for type play.api.libs.functional.FunctionalBuilder[play.api.libs.json.OWrites]#CanBuild2[String,Option[Boolean]].
Try to implement an implicit Writes or Format for this type.
[error] (__ \ "box").write(
I think its just a typo in your writes. you have defaul vs default
I was able to use
import play.api.libs.json._
import play.api.libs.functional.syntax._
val myWrites = (
(__ \ "box").write(
(
(__ \ "name").write[String] ~
(__ \ "default").writeNullable[Boolean]
).tupled
)
)
myWrites.writes("hi",Some(true))
and I got back
res0: play.api.libs.json.JsObject = {"box":{"name":"hi","default":true}}
Scala newbie here.
I use Play to provide a json API for reading and writing a directory like structure. Therefore I use Scalaz.Tree, which provides ways of traversing, updating and rebuilding the Tree.
Formatting the Tree into json works well:
case class File(id: String = BSONObjectID.generate.toString(), name: String, noteBookId: String = null)
implicit val fileFormat: Format[File] = Json.format[File]
implicit def treeWrites: Writes[Tree[File]] =
new Writes[Tree[File]] {
def writes(o: Tree[File]) = o match {
case Node(file, children) => Json.obj(
"name" -> file.name,
"id" -> file.id,
"children" -> JsArray(children.map(Json.toJson(_))),
"notebookId" -> file.noteBookId
)
}
}
Reading json into a Tree however, fails
implicit def treeReads: Reads[Tree[File]] = (
//(__ \ "children").lazyRead(Reads.seq[File](treeReads)) and
(__ \ "children").read[Tree[File]] and
(__ \ "name").read[String] and
(__ \ "notebookid").read[String] and // <-- this is line 41, where the error message points at!!
(__ \ "id").read[String]
)(apply _)
implicit val treeFormat: Format[Tree[File]] = Format(treeReads, treeWrites)
The error I am getting:
[error] /home/dikken/Development/core-service-spaas/app/models/dirTree.scala:41: overloaded method value apply with alternatives:
[error] [B](f: B => (scalaz.Tree[model.treedir.File], String, String, String))(implicit fu: play.api.libs.functional.ContravariantFunctor[play.api.libs.json.Reads])play.api.libs.json.Reads[B] <and>
[error] [B](f: (scalaz.Tree[model.treedir.File], String, String, String) => B)(implicit fu: play.api.libs.functional.Functor[play.api.libs.json.Reads])play.api.libs.json.Reads[B]
[error] cannot be applied to ((=> Nothing) => scalaz.Tree[Nothing])
[error] (__ \ "id").read[String] and
[error] ^
[error] one error found
[error] (compile:compile) Compilation failed
Does this mean I have to pattern match on a case where I have a Tree of Nothing? And how excatly should I do that?
Any help appreciated! Tx!
I'm going to assume that apply _ is actually File.apply _, which cannot work here. File.apply accepts the parameters of the case class File (of which there are three). With JSON combinators, it is trying to pass the four parameters above to File.apply, which does not mix. It also does not produce a Tree[File]. What you need to do is replace File.apply with a method that accepts (children, notebookid, name, id) as parameters, and produces a Tree[File].
Here's a somewhat crude one that will work:
def jsonToTree(children: Seq[Tree[File]], name: String, notebookid: String, id: String): Tree[File] =
Tree.node(File(id, name, notebookid), children.toStream)
The Reads will now look more like this:
implicit def treeReads: Reads[Tree[File]] = (
(__ \ "children").lazyRead[Seq[Tree[File]]](Reads.seq(treeReads)).orElse(Reads.pure(Nil)) and
(__ \ "name").read[String] and
(__ \ "notebookid").read[String] and
(__ \ "id").read[String]
)(jsonToTree _)
You were closer with the commented out line as well. Because this is a recursive structure, we need to use lazyRead.
Testing:
val js = Json.parse("""{
"id": "1",
"name": "test",
"notebookid": "abc",
"children": [
{
"id": "2",
"name": "test222",
"notebookid": "ijk"
},
{
"id": "3",
"name": "test333",
"notebookid": "xyz"
}
]
}""")
scala> val tree = js.as[Tree[File]]
tree: scalaz.Tree[File] = <tree>
scala> tree.rootLabel
res8: File = File(1,test,abc)
scala> tree.subForest
res9: Stream[scalaz.Tree[File]] = Stream(<tree>, ?)
This can also be done (certainly in different ways) without combinators, as well (provided there is an implicit Reads[File] available):
implicit def treeReads: Reads[Tree[File]] = new Reads[Tree[File]] {
def reads(js: JsValue): JsResult[Tree[File]] = {
js.validate[File] map { case file =>
(js \ "children").validate[Stream[Tree[File]]].fold(
_ => Tree.leaf(file),
children => Tree.node(file, children)
)
}
}
}
I've two classes, User & Address.
case class User(
id: Pk[Long] = NotAssigned,
name: String = "",
email: String = "",
addresses: Seq[Address])
case class Address(
id: Pk[Long] = NotAssigned,
userId: Long,
city: String)
From my controller I've to send all users along with their addresses, like Map[User, List[Address]]. I could able to extract them using anorm (mysql) but then I need to send them as json. Could you please help on how to implement the writes & reads for the above Map[User, List[Address]], Thanks.
That should help
import anorm._
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(
id: Pk[Long] = NotAssigned,
name: String = "",
email: String = "",
addresses: Seq[Address])
case class Address(
id: Pk[Long] = NotAssigned,
userId: Long,
city: String)
// Play does not provide Format[Pk[A]], so you need to define it
implicit def pkReads[A](implicit r: Reads[Option[A]]): Reads[Pk[A]] = r.map { _.map(Id(_)).getOrElse(NotAssigned) }
implicit def pkWrites[A](implicit w: Writes[Option[A]]): Writes[Pk[A]] = Writes(id => w.writes(id.toOption))
implicit val addrFormat = Json.format[Address]
implicit val userFormat = Json.format[User]
Now you can easily serialize a user:
val as = Seq(Address(Id(2), 1, "biim"))
val u = User(Id(1), "jto", "jto#foo.bar", as)
scala> Json.toJson(u)
res6: play.api.libs.json.JsValue = {"id":1,"name":"jto","email":"jto#foo.bar","addresses":[{"id":2,"userId":1,"city":"biim"}]}
As Julien says, you can't just serialize a Map[User, Seq[Address]]. It just does not make sense since User can't be a key in a Json Object.
You can solve this problem by transforming your Map[User, List[Address]] to a List[User], and the JsonWriter will became easy to write.
Something like:
list.map {
case (user, address) => user.copy(addresses = address.toSeq)
}
The "addresses" in User contains the address so you don't really need to send Map[User, List[Address]] back to client. The Json would be an array of serialized user objects and addresses is part of that. If you do want to send back a Map then the type Map[String, List[Address]] makes more sense in Json serialization context. Here is the code to generate Json for List[User]. The output looks like this
[
{
"id": 1,
"name": "John Doe",
"email": "john#email.com",
"addresses": [
{
"id": 1001,
"userId": 1,
"city": "Chicago"
},
{
"id": 1002,
"userId": 1,
"city": "New York"
}
]
},
{
"id": 2,
"name": "Jane Doe",
"email": "jane#email.com",
"addresses": [
{
"id": 1012,
"userId": 1,
"city": "Dallas"
}
]
}
]
Here is the code the would be in you controller. It has implicit Json formatters that are used by Json.toJson.
implicit object PkWrites extends Writes[Pk[Long]] {
def writes(key: Pk[Long]) = Json.toJson(key.toOption)
}
implicit object PkReads extends Reads[Pk[Long]] {
def reads(json: JsValue) = json match {
case l: JsNumber => JsSuccess(Id(l.value.toLong))
case _ => JsSuccess(NotAssigned)
}
}
implicit val AddressWrites: Writes[Address] = (
(JsPath \ "id").write[Pk[Long]] and
(JsPath \ "userId").write[Long] and
(JsPath \ "city").write[String]
)(unlift(Address.unapply))
implicit val AddressReads: Reads[Address] = (
(JsPath \ "id").read[Pk[Long]] and
(JsPath \ "userId").read[Long] and
(JsPath \ "city").read[String]
)(Address.apply _)
implicit val UserWrites: Writes[User] = (
(JsPath \ "id").write[Pk[Long]] and
(JsPath \ "name").write[String] and
(JsPath \ "email").write[String] and
(JsPath \ "addresses").write[List[Address]]
)(unlift(User.unapply))
def makeJson() = Action {
val johnAddr1 = Address(Id(1001), 1, "Chicago")
val johnAddr2 = Address(Id(1002), 1, "New York")
val janeAddr1 = Address(Id(1012), 1, "Dallas")
val john = User(Id(1), "John Doe", "john#email.com", List(johnAddr1, johnAddr2))
val jane = User(Id(2), "Jane Doe", "jane#email.com", List(janeAddr1))
Ok(Json.toJson(List(john, jane)))
// Ok(Json.toJson(map))
}
Using the json API, how can I write a transformer for the following:
{
"name":"John Doe",
"number":22
}
to
{
"name":"John Doe",
"number":22,
"slug":"John-Doe-22"
}
This is doable using string manipulation, but how do I pick the values from the 2 fields and apply that to them?
You can do this:
val json = Json.obj(
"name" -> JsString("John Doe"),
"number" -> JsNumber(22)
)
val transformed =
json.transform(
__.json.update(
(__ \ 'slug).json.put(JsString(
(json \ "name").as[String].replace(' ', '-') + "-" + (json \ "number").as[Int]
))
)
).get
Ok(transformed.toString())
//{"name":"John Doe","number":22,"slug":"John-Doe-22"}
Here is nice post about play json trasformations.