Custom JodaTime serializer using Play Framework's JSON library?
Asked Answered
J

3

18

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] and Writes[T] for case classes seems fairly straightforward, but I can't figure out how to do the same for DateTime.

Jolynjolynn answered 15/8, 2013 at 15:0 Comment(0)
S
19

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]
}
Shirashirah answered 18/2, 2015 at 15:1 Comment(5)
Hi, I've tried your code, but it doesn't work for me, I've created a new post about this issueGaskin
In your code use Json.fromJson[User](value) instead of Json.fromJson(value). More details see in answer to your post.Shirashirah
Did you mean "yyyy-MM-dd'T'HH:mm:ssZ" instead of "yyyy-MM-dd'T'HH:mm:ss'Z'"?Kennet
@JakeGreene: Well, it depends of the time string format defined in your project. In my app I used "yyyy-MM-dd'T'HH:mm:ss'Z'".Shirashirah
Yeah this didn't work for me either, I had to use the code in @Guillaume's postGrandpapa
F
25

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())
  }
Fineable answered 16/8, 2013 at 6:11 Comment(2)
Great answer. I had to make a modification to get it working though. 'pattern' is referred to but is not in scope. I replaced it with 'df'. Was that your intention?Colonial
@Colonial Thanks! df doesn't make sense there, actually. I fixed it.Fineable
S
19

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]
}
Shirashirah answered 18/2, 2015 at 15:1 Comment(5)
Hi, I've tried your code, but it doesn't work for me, I've created a new post about this issueGaskin
In your code use Json.fromJson[User](value) instead of Json.fromJson(value). More details see in answer to your post.Shirashirah
Did you mean "yyyy-MM-dd'T'HH:mm:ssZ" instead of "yyyy-MM-dd'T'HH:mm:ss'Z'"?Kennet
@JakeGreene: Well, it depends of the time string format defined in your project. In my app I used "yyyy-MM-dd'T'HH:mm:ss'Z'".Shirashirah
Yeah this didn't work for me either, I had to use the code in @Guillaume's postGrandpapa
R
9

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())
  )
)
Rhombic answered 18/9, 2013 at 5:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.