Custom JodaTime serializer using Play Framework's JSON library? - json

How do I implement a custom JodaTime's DateTime serializer/deserializer for JSON? I'm inclined to use the Play Framework's JSON library (2.1.1). There is a default DateTime serializer, but it uses dt.getMillis instead of .toString which would return an ISO compliant String.
Writing Reads[T] amd Writes[T] for case classes seems fairly straightforward, but I can't figure out how to do the same for DateTime.

There is a default DateTime serializer, but it uses dt.getMillis instead of .toString which would return an ISO compliant String.
If you look at the source, Reads.jodaDateReads already handles both numbers and strings using DateTimeFormatter.forPattern. If you want to handle ISO8601 string, just replace it with ISODateTimeFormat:
implicit val jodaISODateReads: Reads[org.joda.time.DateTime] = new Reads[org.joda.time.DateTime] {
import org.joda.time.DateTime
val df = org.joda.time.format.ISODateTimeFormat.dateTime()
def reads(json: JsValue): JsResult[DateTime] = json match {
case JsNumber(d) => JsSuccess(new DateTime(d.toLong))
case JsString(s) => parseDate(s) match {
case Some(d) => JsSuccess(d)
case None => JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date.isoformat", "ISO8601"))))
}
case _ => JsError(Seq(JsPath() -> Seq(ValidationError("validate.error.expected.date"))))
}
private def parseDate(input: String): Option[DateTime] =
scala.util.control.Exception.allCatch[DateTime] opt (DateTime.parse(input, df))
}
(simplify as desired, e.g. remove number handling)
implicit val jodaDateWrites: Writes[org.joda.time.DateTime] = new Writes[org.joda.time.DateTime] {
def writes(d: org.joda.time.DateTime): JsValue = JsString(d.toString())
}

I use Play 2.3.7 and define in companion object implicit reads/writes with string pattern:
case class User(username:String, birthday:org.joda.time.DateTime)
object User {
implicit val yourJodaDateReads = Reads.jodaDateReads("yyyy-MM-dd'T'HH:mm:ss'Z'")
implicit val yourJodaDateWrites = Writes.jodaDateWrites("yyyy-MM-dd'T'HH:mm:ss'Z'")
implicit val userFormat = Json.format[User]
}

Another, perhaps simpler, solution would be to do a map, for example:
case class GoogleDoc(id: String, etag: String, created: LocalDateTime)
object GoogleDoc {
import org.joda.time.LocalDateTime
import org.joda.time.format.ISODateTimeFormat
implicit val googleDocReads: Reads[GoogleDoc] = (
(__ \ "id").read[String] ~
(__ \ "etag").read[String] ~
(__ \ "createdDate").read[String].map[LocalDateTime](x => LocalDateTime.parse(x, ISODateTimeFormat.basicdDateTime()))
)(GoogleDoc)
}
UPDATE
If you had a recurring need for this conversion, then you could create your own implicit conversion, it is only a couple of lines of code:
import org.joda.time.LocalDateTime
import org.joda.time.format.ISODateTimeFormat
implicit val readsJodaLocalDateTime = Reads[LocalDateTime](js =>
js.validate[String].map[LocalDateTime](dtString =>
LocalDateTime.parse(dtString, ISODateTimeFormat.basicDateTime())
)
)

Related

Making Reads and Writes in Scala Play for lists of custom classes

So I have two classes in my project
case class Item(id: Int, name: String)
and
case class Order(id: Int, items: List[Item])
I'm trying to make reads and writes properties for Order but I get a compiler error saying:
"No unapply or unapplySeq function found"
In my controller I have the following:
implicit val itemReads = Json.reads[Item]
implicit val itemWrites = Json.writes[Item]
implicit val listItemReads = Json.reads[List[Item]]
implicit val listItemWrites = Json.writes[List[Item]]
The code works for itemReads and itemWrites but not for the bottom two. Can anyone tell me where I'm going wrong, I'm new to Play framework.
Thank you for your time.
The "No unapply or unapplySeq function found" error is caused by these two:
implicit val listItemReads = Json.reads[List[Item]]
implicit val listItemWrites = Json.writes[List[Item]]
Just throw them away. As Ende said, Play knows how to deal with lists.
But you need Reads and Writes for Order too! And since you do both reading and writing, it's simplest to define a Format, a mix of the Reads and Writes traits. This should work:
case class Item(id: Int, name: String)
object Item {
implicit val format = Json.format[Item]
}
case class Order(id: Int, items: List[Item])
object Order {
implicit val format = Json.format[Order]
}
Above, the ordering is significant; Item and the companion object must come before Order.
So, once you have all the implicit converters needed, the key is to make them properly visible in the controllers. The above is one solution, but there are other ways, as I learned after trying to do something similar.
You don't actually need to define those two implicits, play already knows how to deal with a list:
scala> import play.api.libs.json._
import play.api.libs.json._
scala> case class Item(id: Int, name: String)
defined class Item
scala> case class Order(id: Int, items: List[Item])
defined class Order
scala> implicit val itemReads = Json.reads[Item]
itemReads: play.api.libs.json.Reads[Item] = play.api.libs.json.Reads$$anon$8#478fdbc9
scala> implicit val itemWrites = Json.writes[Item]
itemWrites: play.api.libs.json.OWrites[Item] = play.api.libs.json.OWrites$$anon$2#26de09b8
scala> Json.toJson(List(Item(1, ""), Item(2, "")))
res0: play.api.libs.json.JsValue = [{"id":1,"name":""},{"id":2,"name":""}]
scala> Json.toJson(Order(10, List(Item(1, ""), Item(2, ""))))
res1: play.api.libs.json.JsValue = {"id":10,"items":[{"id":1,"name":""},{"id":2,"name":""}]}
The error you see probably happens because play uses the unapply method to construct the macro expansion for your read/write and List is an abstract class, play-json needs concrete type to make the macro work.
This works:
case class Item(id: Int, name: String)
case class Order(id: Int, items: List[Item])
implicit val itemFormat = Json.format[Item]
implicit val orderFormat: Format[Order] = (
(JsPath \ "id").format[Int] and
(JsPath \ "items").format[JsArray].inmap(
(v: JsArray) => v.value.map(v => v.as[Item]).toList,
(l: List[Item]) => JsArray(l.map(item => Json.toJson(item)))
)
)(Order.apply, unlift(Order.unapply))
This also allows you to customize the naming for your JSON object. Below is an example of the serialization in action.
Json.toJson(Order(1, List(Item(2, "Item 2"))))
res0: play.api.libs.json.JsValue = {"id":1,"items":[{"id":2,"name":"Item 2"}]}
Json.parse(
"""
|{"id":1,"items":[{"id":2,"name":"Item 2"}]}
""".stripMargin).as[Order]
res1: Order = Order(1,List(Item(2,Item 2)))
I'd also recommend using format instead of read and write if you are doing symmetrical serialization / deserialization.

Parsing json collection using play scala reads

Assume that I have a case class:
case class Element(name: String)
and a companion class which has a reads:
object Element {
implicit val elementReads = (
(JsPath / "name").read[String]
)
}
I need a map of this objects, so:
object ElementMap {
def apply(elements: Iterable[Element]) = {
val builder = Map.newBuilder[String, Element]
elements.foreach { x => builder += ((x.name, x)) }
builder result
}
}
Finally, I have to create a reads that would read from JSON:
implicit val elementMapReads = (
(JsPath \ "elements").read[Seq[Element]]
)(ElementMap(_))
However I get a error message here:
overloaded method value read with alternatives: (t: Seq[Element])play.api.libs.json.Reads[Seq[Element]] <and> (implicit r: play.api.libs.json.Reads[Seq[Element]])play.api.libs.json.Reads[Seq[Element]] cannot be applied to (Iterable[Element] ⇒ Map[String,Element])
This is a working version:
case class Element(name: String)
object Element {
implicit val elementReads: Reads[Element] =
(JsPath \ "name").read[String].map(Element.apply)
implicit val elementMapReads: Reads[Map[String, Element]] =
(JsPath \ "elements").read[Seq[Element]].map(ElementMap(_))
}
object ElementMap {
def apply(elements: Iterable[Element]) = {
val builder = Map.newBuilder[String, Element]
elements.foreach { x => builder += ((x.name, x)) }
builder result
}
}
In your code, the elementReads implicit as it is defined is a Reads[String], not a Reads[Element]; in general it's better to have explicit type annotations in play-json as there are some implicit conversions going on that need them.
Also, Reads[_] has a map method that is convenient when you have single-field wrappers and that solves also the problem of converting a Reads[Seq[Element]] to create a Reads[Map[String, Element]].
Finally, moving the elementMapReads in the Element companion object should make it automatically available (i.e. no import required when using it); I didn't test it but it should work as far as I know.

How to convert java.time.LocalDate to JSValue in Play?

I'm using Play 2.3.x. The code is writing an object into JSON.
How to convert a model, which has a field of java.time.LocalDate, to JSValue?
case class ModelA(id: Int, birthday: LocalDate)
implicit val modelAWrites: Writes[ModelA] = (
(JsPath \ "id").write[Int] and
(JsPath \ "birthday").write[LocalDate]
)(unlift(ModelA.unapply))
The compiler complains that :
No Json serializer found for type java.time.LocalDate. Try to implement an implicit Writes or Format for this type.
Thanks.
If representing the LocalDate as an ISO date string (e.g. "2016-07-09") is sufficient, then the formatter becomes quite simple:
implicit val localDateFormat = new Format[LocalDate] {
override def reads(json: JsValue): JsResult[LocalDate] =
json.validate[String].map(LocalDate.parse)
override def writes(o: LocalDate): JsValue = Json.toJson(o.toString)
}
Here's a free test to prove it:
package com.mypackage
import java.time.LocalDate
import org.scalatest.{Matchers, WordSpecLike}
import play.api.libs.json.{JsString, Json}
class MyJsonFormatsSpec extends WordSpecLike with Matchers {
import MyJsonFormats._
"MyJsonFormats" should {
"serialize and deserialize LocalDates" in {
Json.toJson(LocalDate.of(2016, 7, 9)) shouldEqual JsString("2016-07-09")
JsString("2016-07-09").as[LocalDate] shouldEqual LocalDate.of(2016, 7, 9)
}
}
}
Play is able to write most primitive data types to Json, such as Ints, Strings, and so on. It however is unable to write random types to Json, which is fair enough since otherwise the framework would have to provide a serializer for any type which seems a bit unrealistic!
So, Play is telling you it doesn't know how to serialize a type of java.time.LocalDate. You need to teach Play how to write an instance of LocalDate to Json.
See here for docs on how to do that: https://www.playframework.com/documentation/2.3.x/ScalaJsonCombinators
Instead using LocalDate form java.time.LocalDate use LocalDate from org.joda.time.LocalDate or write your custom implicit
implicit val dateFormat =
Format[LocalDate](Reads.jodaLocalDateReads(pattern), Writes.jodaLocalDateWrites(pattern))
I've had this problem too, I solved it by creating the Reads and Writes myself.
implicit val localDateReads: Reads[LocalDate] = (
(__ \ "year").read[Int] and
(__ \ "month").read[Int] and
(__ \ "day").read[Int]
) (LocalDate.of(_,_,_))
implicit val LocalDateWrites = new Writes[LocalDate] {
def writes(date: LocalDate) = Json.obj(
"day" -> date.getDayOfMonth,
"month" -> date.getMonthValue,
"year" -> date.getYear
)}
In JSON it will look like this:
"date": {
"day": 29,
"month": 8,
"year": 1993
}

How can I write dates to ISO 8601 format using JSON Writes?

I've got a case calss
import java.sql.Date
case class GetMilestoneLanguage(
...
due_date: Option[Date],
...
)
object GetMilestoneLanguage {
implicit val writes = Json.writes[GetMilestoneLanguage]
}
Its outputting the JSON in UTC - and I need it to be iso 8601. I'm NOT use Joda time.
What the easiest way to achieve getting the date in iso 8601?
Thanks
import play.api.libs.json.{Json, Writes}
import play.api.libs.json.Writes.dateWrites // do not import everything here, especially DefaultDateWrites
case class GetMilestoneLanguage(param1: String, dueDate: Option[java.sql.Date])
object GetMilestoneLanguage {
implicit val customDateWrites: Writes[java.util.Date] = dateWrites("yyyy-MM-dd'T'HH:mm:ss'Z'")
implicit val writes = Json.writes[GetMilestoneLanguage]
}
The key here is to define your own implicit Writes[java.util.Date]. If you import DefaultDateWrites your customDateWrites will be silently ignored (I wonder why there is no ambiguous implicit warning).
You can create a custom Writes[java.util.Date] using the helper provided on the Writes companion object. You won't be able to use the Json.writes macro helper though.
import play.api.libs.json._
import play.api.libs.functional.syntax._
import play.api.libs.json.Writes._
case class GetMilestoneLanguage(param1: String, dueDate: Option[Date], param3: String)
object GetMilestoneLanguage {
implicit val writes = (
(__ \ "param1").write[String] and
(__ \ "due_date").write(dateWrites("yyyy-MM-dd'T'HH:mm:ss'Z'")) and
(__ \ "param3").write[String]
)(unlift(GetMilestoneLanguage.unapply))
}

How do I create a JSON format for Map[Int, Int] using Play Framework JSON libs?

I'd like to serialize a Map[Int, Int] using the Play Framework JSON libraries. I want something like
import play.api.libs.json._
implicit val formatIntMap = Json.format[Map[Int, Int]]
That code however gets an No unapply function found which I think is referring to the fact that there is no extractor for Map since it isn't a simple case class.
It will try to make a JsObject but is failing as it maps a String to a JsValue, rather than an Int to a JsValue. You need to tell it how to convert your keys into Strings.
implicit val jsonWrites = new Writes[Map[Int, Int]] {
def writes(o: Map[Int, Int]): JsValue = {
val keyAsString = o.map { kv => kv._1.toString -> kv._2} // Convert to Map[String,Int] which it can convert
Json.toJson(keyAsString)
}
}
This will turn a Map[Int, Int] containing 0 -> 123 into
JsObject(
Seq(
("0", JsNumber(123))
)
)