I try to migrate my web application from Play 2.0.4 to Play 2.1-RC2.
I have JSON data with a list of unknown keys (key1, key2) like this:
{description: "Blah",
tags: [
key1: ["value1", "value2"],
key2: ["value3"]
]
}
I want to store the data from the JSON in a List of Metatags. In Play 2.0.4 I have used something like this to read the tags-list:
def readMetatags(meta: JsObject): List[Metatag] =
meta.keys.toList.map(x => Metatag(x, (meta \ x).as[List[String]])
Now I want to use the new Play 2.1-JSON-API (Prototype):
import play.api.libs.json._
import play.api.libs.functional.syntax._
object Metatags {
implicit val metatagsRead: Read[Metatags] = (
(JsPath \ "description").read[String] and
(JsPath \ "tags").read[List[Metatag]]
)(Metatags.apply _, unlift(Metatags.unapply _))
implicit val metatagRead: Read[Metatag] = (
JsPath().key. ?? read[String] and // ?!? read key
JsPath().values. ?? read[List[String]] // ?!? read value list
)(Metatag.apply _, unlift(Metatag.unapply _))
}
case class Metatags(description: String, tags: List[Metatag])
case class Metatag(key: String, data: List[String])
How can I read the keys from the JSON?
This is a solution with a custom reader for the MetaTag class. The read just convert the JsValue to a JsObject, which have the useful fieldSet method.
For MetaTags, the macro inception works perfectly
object Application extends Controller {
case class MetaTags(description: String, tags: List[MetaTag])
case class MetaTag(key: String, data: List[String])
implicit val readMetaTag = Reads(js =>
JsSuccess(js.as[JsObject].fieldSet.map(tag =>
MetaTag(tag._1, tag._2.as[List[String]])).toList))
implicit val readMetaTags = Json.reads[MetaTags]
def index = Action {
val json = Json.obj(
"description" -> "Hello world",
"tags" -> Map(
"key1" -> Seq("key1a", "key1b"),
"key2" -> Seq("key2a"),
"key3" -> Seq("Key3a", "key3b", "key3c")))
val meta = json.as[MetaTags]
Ok(meta.tags.map(_.data.mkString(",")).mkString("/"))
// key1a,key1b/key2a/Key3a,key3b,key3c
}
}
Related
I have read this question (and the other ones on SO), but I still don't manage to convert a JsValue to a case class with a Joda DateTime.
Here is the JSON I have:
val json = Json.parse(
"""
{
"message": "Bla bla bla",
"updated_time": "2016-09-17T12:48:12+0000"
}
"""
)
And the corresponding case class is:
import org.joda.time.DateTime
final case class FacebookPost(message: String, updated_time: DateTime)
I also added this implicit DateTime reader (I tried some other as well):
implicit val readsJodaLocalDateTime = Reads[DateTime](js =>
js.validate[String].map[DateTime](dtString => new DateTime(dtString)))
But when I try to convert my json to the corresponding case class (json.as[FacebookPost]), I get this error:
play.api.libs.json.JsResultException: `JsResultException(errors:List((/updated_time,List(ValidationError(List(error.expected.jodadate.format),WrappedArray(yyyy-MM-dd))))))`
What am I doing wrong?
Your problem is the format of your datetime: 2016-09-17T12:48:12+0000.
By default, Joda's DateTime class can't automatically figure out the format you're using, so you have to provide a hint using DateTimeFormat.
Example for our custom Reads[DateTime]:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
val dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
val customReads = Reads[DateTime](js =>
js.validate[String].map[DateTime](dtString =>
DateTime.parse(dtString, DateTimeFormat.forPattern(dateFormat))
)
)
implicit val reads: Reads[FacebookPost] = (
(__ \ "message").read[String] and
(__ \ "updated_time").read[DateTime](customReads)
)(FacebookPost.apply _)
Then, it's possible to do the following:
val json = Json.parse(
"""
{
"message": "Bla bla bla",
"updated_time": "2016-09-17T12:48:12+0000"
}
""")
val post = json.validate[FacebookPost]
// => JsSuccess(FacebookPost(Bla bla bla,2016-09-17T14:48:12.000+02:00))
edit:
You don't need to create the Read[DateTime] from scratch, there is a predefined method in Play that helps you with that:
object CustomReads /* or wherever you want to define it */ {
val dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
implicit val jodaDateReads = play.api.libs.json.Reads.jodaDateReads(dateFormat)
}
Done. Just import whenever you need it, e.g. in the companion object of your model classes:
object FacebookPost {
import utils.CustomReads._
implicit val reads: Reads[FacebookPost] = Json.reads[FacebookPost]
}
You can "override" the implicit Read[DateTime] provided by Play by exluding it from your imports:
import play.api.libs.json.Reads.{DefaultJodaDateReads => _, _}
{
"location":{
"residents":[{
"renting":[{
"name":"John Doe"
"pets":"2"
},{
"name":"Jane Smith"
"pets":"2"
}]
}]
}
}
I can successfully traverse location with this -
val json = ...
val rentReads = (__ \ 'location).read[String]
val rentResult = json.validate[String](rentReads)
rentResult match {
case s: JsSuccess[String] => Ok(s.get)
case e: JsError => Ok("Errors: " + JsError.toFlatJson(e).toString())
}
Based on the documentation, I should be able to do something like this -
val skillReads = ((__ \ 'location) \ 'residents)(0).read[String]
but it results in the following error -
Errors: {"obj.VariationResultsWrapper.VariationResultSets[0]":[{"msg":"error.path.missing","args":[]}]}
At this point I'm just trying to understand how to return values from "renting" only. Eventually, I would like to map that result to a case class.
If your eventual goal is to parse this into case classes, just define those case classes and let Play do the heavy lifting.
case class Renting(name: String, pets: String)
case class Resident(renting: List[Renting])
case class Location(residents: List[Resident])
implicit val rentingFormat = Json.format[Renting]
implicit val residentFormat = Json.format[Resident]
implicit val locationFormat = Json.format[Location]
(json \ "location").validate[Location]
res1: play.api.libs.json.JsResult[Location] = JsSuccess(Location(List(Resident(List(Renting(John Doe,2), Renting(Jane Smith,2))))),/residents)
I have a collection of objects like:
class Car(val id: Int, val name: String)
I will then have a collection of cars in a List
List[Car]();
I want to be able to convert the List[Car] into a JSON string, and then load it from a JSON string back.
As a side note, how volatile is this conversion process, say I add another property to my User object? (Note: I don't want an answer that solves this issue, just wondering if adding an item would break it)
Update
How to handle Maps correctly like: Map[Int, Long]
Say my model is updated with the Map included:
class Car(val id: Int, val name: String, val options: Map[Int, Long])
Using play's json I create my Writes implicit like:
implicit val carWrites = new Writes[Car] {
def writes(c: Car) = Json.obj{
"id" -> c.id
"name" -> c.name
//"options" -> t.options
}
}
Currently I see an error: No Json serializer found for type Map[Int,Long]
case class Car(id: Int, name: String, options: Map[Int, Long])
object Car{
implicit val carWrites = new Writes[Car] {
def writes(c: Car) = Json.obj(
"id" -> c.id,
"name" -> c.name,
"options" -> c.options.map(item=>{
Json.obj(
"id" -> item._1,
"name" -> item._2
)
})
)
}
implicit val carReads =
(__ \ "id").read[Int] ~
(__ \ "name").read[String] ~
(__ \ "options").read(
(__ \ "id").read[Int] ~
(__ \ "name").read[Long]
)
(Car)
}
And then to output it, if using Play:
val x = Car(1,"Porsche",Map(0->1,1->2.toLong))
val json = Json.toJson(x)
Ok(json).as("application/json")
Are you using any existing toolkits to do this? I've got experience with Spray-Json. You don't have to use the entire Spray toolkit, you can just use Spray-Json by itself. Your marshalling would be simple.
object UserJsonProtocol extends DefaultJsonProtocol {
implicit val userFormat = jsonFormat3(User)
}
import UserJsonProtocol._
val jsonFromList = listUsers.toJson
val usersFromJsonString = usersString.parseJson.convertTo[List[User]]
Adding a field to the User class only requires updating the jsonFormat to the correct number of fields.
With the play-json library (available as standalone library) you are free to add fields to the User class without even needing to modify the play-json Reads/Writes/Formats that are used to serialize/deserialize, e.g. of type Int, String etc. However if you add a custom type then you'd need to write a custom serializer for that field of course.
If you want to make changes that are backwards compatible with clients unaware of the new fields then set them as type Option and there should be no need to do anything more.
This would work even if a client sends JSON without the customField:
class User(val id: Int, val name: String, val age: Int, val customField: Option[String])
However this would fail if client does not include customField (Play JSON does not set customField to null as would be done say by Gson for Java):
class User(val id: Int, val name: String, val age: Int, val customField: String)
Cheers,
If you're using Play framework then
case class User(val id: Int, val name: String, val age: Int)
implicit val userReads : Reads[User] = (
(__ \ "id").read[Int] ~
(__ \ "name").read[String] ~
(__ \ "age").read[Int]
)(User.apply _)
play.api.libs.json.Json.fromJson[User](incomingJson)
Similarly define Writes[User]
I'm using play-json's macros to define implicit Writes for serializing JSON. However, it seems like by default play-json omits fields for which Option fields are set to None. Is there a way to change the default so that it outputs null instead? I know this is possible if I define my own Writes definition, but I'm interested in doing it via macros to reduce boilerplate code.
Example
case class Person(name: String, address: Option[String])
implicit val personWrites = Json.writes[Person]
Json.toJson(Person("John Smith", None))
// Outputs: {"name":"John Smith"}
// Instead want to output: {"name":"John Smith", "address": null}
The Json.writes macro generates a writeNullable[T] for optional fields. Like you know (or not), writeNullable[T] omits the field if the value is None, whereas write[Option[T]] generates a null field.
Defining a custom writer is the only option you have to get this behavior.
(
(__ \ 'name).write[String] and
(__ \ 'address).write[Option[String]]
)(unlift(Person.unapply _))
You can use a custom implicit JsonConfiguration, see Customize the macro to output null
implicit val config = JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
implicit val personWrites = Json.writes[Person]
Json.toJson(Person("John Smith", None))
Not a real solution for you situation. But slightly better than having to manually write the writes
I created a helper class that can "ensure" fields.
implicit class WritesOps[A](val self: Writes[A]) extends AnyVal {
def ensureField(fieldName: String, path: JsPath = __, value: JsValue = JsNull): Writes[A] = {
val update = path.json.update(
__.read[JsObject].map( o => if(o.keys.contains(fieldName)) o else o ++ Json.obj(fieldName -> value))
)
self.transform(js => js.validate(update) match {
case JsSuccess(v,_) => v
case err: JsError => throw new JsResultException(err.errors)
})
}
def ensureFields(fieldNames: String*)(value: JsValue = JsNull, path: JsPath = __): Writes[A] =
fieldNames.foldLeft(self)((w, fn) => w.ensureField(fn, path, value))
}
so that you can write
Json.writes[Person].ensureFields("address")()
Similar answer to above, but another syntax for this:
implicit val personWrites = new Writes[Person] {
override def writes(p: Person) = Json.obj(
"name" -> p.name,
"address" -> p.address,
)
}
This is simple:
implicit val personWrites = new Writes[Person] {
override def writes(p: Person) = Json.obj(
"name" -> p.name,
"address" -> noneToString(p.address),
)
}
def optToString[T](opt: Option[T]) =
if (opt.isDefined) opt.get.toString else "null"
You can define something like this :
implicit class JsPathExtended(path: JsPath) {
def writeJsonOption[T](implicit w: Writes[T]): OWrites[Option[T]] = OWrites[Option[T]] { option =>
option.map(value =>
JsPath.createObj(path -> w.writes(value))
).getOrElse(JsPath.createObj(path -> JsNull))
}
}
And if you are using play framework :
implicit val totoWrites: Writes[Toto] = (
(JsPath \ "titre").write[String] and
(JsPath \ "option").writeJsonOption[String] and
(JsPath \ "descriptionPoste").writeNullable[String]
) (unlift(Toto.unapply))
implicit val totoReads: Reads[Toto] = (
(JsPath \ "titre").read[String] and
(JsPath \ "option").readNullable[String] and
(JsPath \ "descriptionPoste").readNullable[String]
) (Toto.apply _)
You may wrap your option and redefine serialization behavior:
case class MyOption[T](o: Option[T])
implicit def myOptWrites[T: Writes] = Writes[MyOption[T]](_.o.map(Json.toJson).getOrElse(JsNull))
CAVEATS:
1)this approach is good only for case classes used solely as a serialization protocol definition. If you are reusing some data model classes in controllers - define custom serializers for them not to pollute the model.
2)(related only to Option) if you use the same class for writes and reads. Play will require the wrapped fields to be present (possibly null) during deserialization.
P.S.: Failed to do the same with type tagging. Compiler error is like
No instance of play.api.libs.json.Writes is available for tag.<refinement> (given the required writes were explicitly defined).
Looks like Play's macro fault.
This question is based on the following example, which is an attempt to deserialize a case class Node[Bird] from JSON.
import play.api.libs.json._
import play.api.libs.functional.syntax._
object ValidationTest {
case class Node[T](_id: Int, thing: T)
case class Bird(name: String, color: String)
// ERROR -- No apply function found matching unapply parameters
implicit val birdNodeFormat = Json.format[Node[Bird]]
val birdNodesJson: JsValue = Json.arr(
Json.obj(
"_id" -> 0,
"thing" -> Json.obj(
"name" -> "Cardinal",
"color" -> "Red"
)
),
Json.obj(
"_id" -> 1,
"thing" -> Json.obj(
"name" -> "Bluejay",
"color" -> "Blue"
)
)
)
val birdNodesResult = birdNodesJson.validate[Seq[Node[Bird]]]
}
In the preceding example, Scala is unable to resolve the proper apply/unapply functions for Node[Bird] for the format macro.
// ERROR -- No apply function found matching unapply parameters
implicit val birdNodeFormat = Json.format[Node[Bird]]
However, there is no problem with using a non-generic case class such as BirdNode.
case class BirdNode(_id: Int, thing: Bird)
// WORKS
implicit val birdNodeFormat = Json.format[BirdNode]
...
// WORKS
val birdNodesResult = birdNodesJson.validate[Seq[BirdNode]]
What is the best way to serialize/deserialize something like a Node[Bird] to/from JSON in Play 2.2?
You might have to define the format for Node[T] without using the macro, but this works:
implicit val birdFormat: Format[Bird] = Json.format[Bird]
implicit def nodeFormat[T : Format]: Format[Node[T]] =
((__ \ "_id").format[Int] ~
(__ \ "thing").format[T]
)(Node.apply, unlift(Node.unapply))