11

In Play 2.3, I have a case class with a single optional double member:

case class SomeClass(foo: Option[Double])

I need a JSON write converter that handles the member as nullable:

implicit val someClassWrite: Writes[SomeClass] = ???

The Play docs provide an example:

case class DisplayName(name:String)
implicit val displayNameWrite: Writes[DisplayName] = Writes {
  (displayName: DisplayName) => JsString(displayName.name)
}

But sadly I can't figure out how to do this for 1) a single nullable and 2) a double. Any ideas? Thanks.

Update #1: The only solution I can come up with is this:

implicit val someClassWrite: Writes[SomeClass] = Writes {
  (someClass: SomeClass) => someClass.foo match {
    case Some(f) => JsNumber(BigDecimal(f))
    case _ => JsNull
}

Update #2: Ignore my solution. Travis Brown's is the one.

biesior
  • 55,576
  • 10
  • 125
  • 182
Lasf
  • 2,536
  • 1
  • 16
  • 35
  • All the play docs example does is transform a `String` to a `JsString`, to which there is no analog for `Option[Double]` in JSON. Could you provide some more context? – Michael Zajac Oct 20 '14 at 02:05
  • 1
    @LimbSoup See those same docs: `case class DisplayName(name:String) val nameReads: Reads[DisplayName] = (JsPath \ "name").read[String].map(DisplayName(_))`. That works perfectly for a reads converter, by using `.readNullable[Double]` instead. I'm looking for the equivalent writes converter. – Lasf Oct 20 '14 at 02:13
  • `writeNullable[Double]` ?? – Michael Zajac Oct 20 '14 at 02:16
  • @LimbSoup I wish... `value map is not a member of play.api.libs.json.OWrites[Option[Double]]` – Lasf Oct 20 '14 at 02:28
  • What are you trying to map? Like I said, more context is needed.. – Michael Zajac Oct 20 '14 at 02:38
  • @LimbSoup I don't know, because I'm not familiar with how Plays OReads/OWrites works. I'm simply following the docs, which works in the reads case, but not the writes case. I appreciate your help with this; it is an esoteric Play libs question, and thus the context lies within those Play libs, and so for me to provide you more context would require me being more deeply familiar with how the play Json libs work, which is entirely my problem. – Lasf Oct 20 '14 at 02:45

2 Answers2

26

Writes isn't a covariant functor, so you can't use map, but you can use contramap:

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit val someClassWrites: Writes[SomeClass] =
  (__ \ 'foo).writeNullable[Double].contramap(_.foo)

If you have more than one member, you can use Play's FunctionalBuilder syntax:

case class AnotherClass(foo: Option[Double], bar: Option[String])

implicit val anotherClassWrites: Writes[AnotherClass] = (
  (__ \ 'foo).writeNullable[Double] and
  (__ \ 'bar).writeNullable[String]
)(ac => (ac.foo, ac.bar))

In the first case the argument to contramap is just a function from the type you want a Writes for to the type in the Writes you're calling contramap on. In the second case, the function at the end is from the target (AnotherClass here) to a tuple of the Writes instances you've built up with and (in this case Option[Double] and Option[String]).

Travis Brown
  • 138,631
  • 12
  • 375
  • 680
  • is this documented anywhere? I cant find it and have been fighting this for more than should be necessary lol . the contramap bit that is – Mark Aug 03 '18 at 15:58
2

The easy way is:

import play.api.libs.json.Json

implicit val fmt = Json.format[SomeClass]

Which uses a macro to auto-generate the json format for you. Beats implementing writes directly.

triggerNZ
  • 4,221
  • 3
  • 28
  • 34
  • I should add that it's part of a complex system of Writes... recursive writes etc. Is there a hard way? – Lasf Oct 20 '14 at 01:41
  • 1
    If your case class' property is `someId:String` and the JSON element containing your `someId` is "some-ID" then you can't use the macro-generated format. – Peter Perháč Jan 30 '17 at 18:00