0
case class Apple(id:String, name:String)
case class Fruit(id:String,ftype:String)

case class Basket(b:Map[Fruit,Apple])

How to define the play implicits as the below definition are not enough.

 implicit val format: Format[Fruit] = Json.format
 implicit val format: Format[Apple] = Json.format

This isn't working :

 implicit val format: Format[Basket] = Json.format
supernatural
  • 1,107
  • 11
  • 34

2 Answers2

2

The formatter are ok, but they only work for Case Classes.

So all you have to do is to adjust them:

case class Apple(id:String, name:String)
case class Fruit(id:String,ftype:String)

case class Basket(b:Map[Fruit,Apple])

Update

Ok there is another problem. JSON has a restriction that the Key of a Map must be a String.

See my answer here: https://stackoverflow.com/a/53896463/2750966

Ok here an example for Play < 2.8:

  implicit val formata: Format[Apple] = Json.format

  implicit val mapReads: Reads[Map[Fruit, Apple]] = (jv: JsValue) =>
    JsSuccess(jv.as[Map[String, Apple]].map { case (k, v) =>
      (k.split("::").toList match {
        case id :: ftype :: _ => Fruit(id, ftype)
        case other => throw new IllegalArgumentException(s"Unexpected Fruit Key $other")
      }) -> v
    })

  implicit val mapWrites: Writes[Map[Fruit, Apple]] = (map: Map[Fruit, Apple]) =>
    Json.toJson(map.map { case (fruit, o) =>
      s"${fruit.id}::${fruit.ftype}" -> o
    })
  implicit val jsonMapFormat: Format[Map[Fruit, Apple]] = Format(mapReads, mapWrites)

  implicit val formatb: Format[Basket] = Json.format

With this example Data it works:

  val basket = Basket(Map(Fruit("12A", "granate") -> Apple("A11", "The Super Apple"),
     Fruit("22A", "gala") -> Apple("A21", "The Gala Premium Apple")))
  val json = Json.toJson(basket) // >> {"b":{"12A::granate":{"id":"A11","name":"The Super Apple"},"22A::gala":{"id":"A21","name":"The Gala Premium Apple"}}}
  json.as[Basket] // >> Basket(Map(Fruit(12A,granate) -> Apple(A11,The Super Apple), Fruit(22A,gala) -> Apple(A21,The Gala Premium Apple)))

Here the Scalafiddle

pme
  • 14,156
  • 3
  • 52
  • 95
  • I am sorry, it was originally case class and it was not working. – supernatural Feb 11 '20 at 14:26
  • see my update - sorry I did not spot that - maybe the exception would help in the question. – pme Feb 11 '20 at 14:59
  • so, if we need the key as a case class/object, should we use the toString method of the case class as a key, is that a good practice @pme – supernatural Feb 11 '20 at 17:59
  • You would need an identifier that is unique. In your case that would be maybe id. But this only works to write a json. To read it you would need to create a string with all the values, and parse them when reading the Json back to the Map. – pme Feb 11 '20 at 19:54
  • Could you please give an example @pme – supernatural Feb 12 '20 at 07:53
  • 1
    As of play-json 2.8 it is no longer true that the key of a Map must be a String. You only need to define a `KeyWrites` and `KeyReads` for the class. – cbley Feb 12 '20 at 11:03
  • 1
    Also, note that the macros work for normal classes as well, not just case classes. The only requirement is that the companion object has matching apply and unapply methods (which case classes automatically fulfill). – cbley Feb 12 '20 at 14:38
  • @potterpod - ok I added an example - if you have Play 2.8, see the answer of cbley – pme Feb 12 '20 at 18:13
1

Here is a possible implementation using Play JSON 2.8:

import play.api.libs.json._

case class Apple(id:String, name:String)
case class Fruit(id:String,ftype:String)

case class Basket(b:Map[Fruit,Apple])

object Apple {
  implicit val format = Json.format[Apple]
}

object Basket {
  implicit val keyReads: KeyReads[Fruit] = s => ???
  implicit val keyWrites: KeyWrites[Fruit] = f => s"${f.id}/${f.ftype}"

  implicit val format = Json.format[Basket]
}

val b = Basket(Map(Fruit("1", "sw") -> Apple("2", "boskop")))

Json.stringify(Json.toJson(b)) // -> {"b":{"1/sw":{"id":"2","name":"boskop"}}}

https://scastie.scala-lang.org/XbFY6amKSb6BCVCrGmgrBQ

cbley
  • 4,538
  • 1
  • 17
  • 32