How do I write a JSON Format for an object in the Java library that doesn't have an apply method?

10,636

It's not necessarily apply or unapply functions you need. It's a) a function that constructs whatever the type you need given some parameters, and b) a function that turns an instance of that type into a tuple of values (usually matching the input parameters.)

The apply and unapply functions you get for free with a Scala case class just happen to do this, so it's convenient to use them. But you can always write your own.

Normally you could do this with anonymous functions like so:

import java.sql.Timestamp
import play.api.libs.functional.syntax._
import play.api.libs.json._

implicit val timestampFormat: Format[Timestamp] = (
  (__ \ "time").format[Long]
)((long: Long) => new Timestamp(long), (ts: Timestamp) => (ts.getTime))

However! In this case you fall foul of a limitation with the API that prevents you from writing formats like this, with only one value. This limitation is explained here, as per this answer.

For you, a way that works would be this more complex-looking hack:

import java.sql.Timestamp
import play.api.libs.functional.syntax._
import play.api.libs.json._

implicit val rds: Reads[Timestamp] = (__ \ "time").read[Long].map{ long => new Timestamp(long) }
implicit val wrs: Writes[Timestamp] = (__ \ "time").write[Long].contramap{ (a: Timestamp) => a.getTime }
implicit val fmt: Format[Timestamp] = Format(rds, wrs)

// Test it...
val testTime = Json.obj("time" -> 123456789)
assert(testTime.as[Timestamp] == new Timestamp(123456789))
Share:
10,636

Related videos on Youtube

Meredith
Author by

Meredith

Updated on June 04, 2022

Comments

  • Meredith
    Meredith almost 2 years

    I've been stuck on this particular problem for about a week now, and I figure I'm going to write this up as a question on here to clear out my thoughts and get some guidance.

    So I have this case class that has a java.sql.Timestamp field:

    case class Request(id: Option[Int], requestDate: Timestamp)

    and I want to convert this to a JsObject

    val q = Query(Requests).list // This is Slick, a database access lib for Scala
      printList(q)    
      Ok(Json.toJson(q))   // and this is where I run into trouble
    

    "No Json deserializer found for type List[models.Request]. Try to implement an implicit Writes or Format for this type." Okay, that makes sense.

    So following the Play documentation here, I attempt to write a Format...

    implicit val requestFormat = Json.format[Request]  // need Timestamp deserializer
    implicit val timestampFormat = (
          (__ \ "time").format[Long]   // error 1
    )(Timestamp.apply, unlift(Timestamp.unapply))  // error 2
    

    Error 1

    Description Resource Path Location Type overloaded method value format with alternatives:   
    
    (w: play.api.libs.json.Writes[Long])(implicit r: play.api.libs.json.Reads[Long])play.api.libs.json.OFormat[Long] 
    <and>   
    (r: play.api.libs.json.Reads[Long])(implicit w: play.api.libs.json.Writes[Long])play.api.libs.json.OFormat[Long] 
    <and>   
    (implicit f: play.api.libs.json.Format[Long])play.api.libs.json.OFormat[Long]  
    cannot be applied to (<error>, <error>)
    

    Apparently importing like so (see the documentation "ctrl+F import") is getting me into trouble:

    import play.api.libs.json._    // so I change this to import only Format and fine
    import play.api.libs.functional.syntax._
    import play.api.libs.json.Json
    import play.api.libs.json.Json._  
    

    Now that the overloading error went away, I reach more trubbles: not found: value __ I imported .../functional.syntax._ already just like it says in the documentation! This guy ran into the same issue but the import fixed it for him! So why?! I thought this might just be Eclipse's problem and tried to play run anyway ... nothing changed. Fine. The compiler is always right.

    Imported play.api.lib.json.JsPath, changed __ to JsPath, and wallah:

    Error 2

    value apply is not a member of object java.sql.Timestamp
    value unapply is not a member of object java.sql.Timestamp


    I also try changing tacks and writing a Write for this instead of Format, without the fancy new combinator (__) feature by following the original blog post the official docs are based on/copy-pasted from:

    // I change the imports above to use Writes instead of Format
     implicit val timestampFormat = new Writes[Timestamp](  // ERROR 3
        def writes(t: Timestamp): JsValue = { // ERROR 4 def is underlined
          Json.obj(
              /* Returns the number of milliseconds since 
               January 1, 1970, 00:00:00 GMT represented by this Timestamp object. */
                  "time" -> t.getTime() 
          )
        }
      )
    

    ERROR 3: trait Writes is abstract, cannot be instantiated

    ERROR 4: illegal start of simple expression

    At this point I'm about at my wits' end here, so I'm just going back to the rest of my mental stack and report from my first piece of code



    My utter gratefulness to anybody who can put me out of my coding misery

  • Meredith
    Meredith almost 11 years
    Shoot now I'm running into a NullPointerException when I'm actually trying toJson()... and I'm not trying to serialize any null values even though that shouldn't cause an error?...
  • Mikesname
    Mikesname almost 11 years
    @MeredithLeu: Make sure that if your Reads/Writes/Formats refer to either themselves or other R/W/F defined later in the same object with circular dependencies, that you use the lazyFormat helpers. Otherwise it can compile but give NPE at runtime. I just ran into this issue myself. See the treatment of recursive format definitions here.
  • Meredith
    Meredith almost 11 years
    But I don't have any circular dependencies in my schema... I have foreign keys to other objects, but none of them point back.
  • Mikesname
    Mikesname almost 11 years
    It's hard to help without seeing more of your code and a stacktrace (in another question, perhaps.) NPE errors in Scala often relate to the order the variables are initialized in a class or object. If you attempt to use one (i.e. in a Reads/Writes/Format) before it is initialised the value will be null and it will crash at Runtime.
  • Meredith
    Meredith almost 11 years
    I got it fixed! It was the Request object itself that was being unhappy with the Timestamp field. This was my hack around it: lazy implicit val requestFormat = Json.format[Request] And that was all! More details are located here.
  • Mikesname
    Mikesname almost 11 years
    Yep, that's the order of initialization gotcha I mentioned. Your Request format is being generated (magically, by the macro) before the timestamp format, which it depends on. I think you could've just stuck the timestamp format code above the line implicit val requestFormat = Json.format[Request] and it would've worked without the lazy modifier.
  • Meredith
    Meredith almost 11 years
    Wow. Yeah that worked. Wow. Okay. Order of implicit val declaration matters. Wow. I spent a few hours on the order of my code. Well, I learned quite a bit at least.