Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Play JSON formatter for Map[Int,_]

I am attempting to migrate a Rails/Mongodb application to Play 2.3 using play-reactivemongo and reactivemongo-extensions. In modeling my data I am running across a problem serializing and deserializing a Map[Int,Boolean].

When I try to define my formats via macro like so

implicit val myCaseClass = Json.format[MyCaseClass]

where MyCaseClass has a few string fields, a BSONObjectID field, and a Map[Int,Boolean] field the compiler complains with:

No Json serializer found for type Map[Int,Boolean]. Try to implement an implicit Writes or Format for this type.
No Json deserializer found for type Map[Int,Boolean]. Try to implement an implicit Reads or Format for this type.

Looking at the source code for Play in Reads.scala I see a Reads defined for Map[String,_] but none for Map[Int,_].

Is there a reason why Play has default Read/Writes for string maps but not for other simple types?

I don't fully understand the Map[String,_] defined by play because I am fairly new to scala. How would I go about translating that into a Map[Int,_]? If that is not possible for some technical reason how would I define a Reads/Writes for Map[Int,Boolean]?

like image 596
imagio Avatar asked Dec 04 '14 03:12

imagio


4 Answers

you can write your own reads and writes in play.

in your case, this would look like this:

implicit val mapReads: Reads[Map[Int, Boolean]] = new Reads[Map[Int, Boolean]] {
    def reads(jv: JsValue): JsResult[Map[Int, Boolean]] =
        JsSuccess(jv.as[Map[String, Boolean]].map{case (k, v) =>
            Integer.parseInt(k) -> v .asInstanceOf[Boolean]
        })
}

implicit val mapWrites: Writes[Map[Int, Boolean]] = new Writes[Map[Int, Boolean]] {
    def writes(map: Map[Int, Boolean]): JsValue =
        Json.obj(map.map{case (s, o) =>
            val ret: (String, JsValueWrapper) = s.toString -> JsBoolean(o)
            ret
        }.toSeq:_*)
}

implicit val mapFormat: Format[Map[Int, Boolean]] = Format(mapReads, mapWrites)

I have tested it with play 2.3. I'm not sure if it's the best approach to have a Map[Int, Boolean] on server side and a json object with string -> boolean mapping on the client side, though.

like image 138
pichsenmeister Avatar answered Oct 23 '22 13:10

pichsenmeister


Play Json provides built-in mapReads and mapWrites for reading and writing Maps.

mapReads takes a (String => JsResult[K]) to let you convert the key to your custom type.

mapWrites returns a Writes[Map[String, Boolean]], and you can use contramap to modify that writer into one that works with a Map[Int, Boolean]

import play.api.libs.json.{JsResult, Reads, Writes}
import scala.util.Try

import play.api.libs.json.Reads.mapReads
import play.api.libs.json.MapWrites.mapWrites

object MapExample {

  implicit val reads: Reads[Map[Int, Boolean]] =
    mapReads[Int, Boolean](s => JsResult.fromTry(Try(s.toInt)))

  implicit val writes: Writes[Map[Int, Boolean]] =
    mapWrites[Boolean].contramap(_.map { case (k, v) => k.toString -> v})
}

like image 32
Jake Avatar answered Oct 01 '22 01:10

Jake


JSON only allows string keys (a limitation it inherits from JavaScript).

like image 8
Seth Tisue Avatar answered Oct 23 '22 13:10

Seth Tisue


Thanks to Seth Tisue. This is my "generics" (half) way.

"half" because it does not handle a generic key. one can copy paste and replace the "Long" with "Int"

"Summary" is a type I've wanted to serialize (and it needed its own serializer)

/** this is how to create reader and writer or format for Maps*/
//  implicit val mapReads: Reads[Map[Long, Summary]] = new MapLongReads[Summary]
//  implicit val mapWrites: Writes[Map[Long, Summary]] = new MapLongWrites[Summary]
implicit val mapLongSummaryFormat: Format[Map[Long, Summary]] = new MapLongFormats[Summary]

This is the required implementation:

class MapLongReads[T]()(implicit reads: Reads[T]) extends Reads[Map[Long, T]] {
  def reads(jv: JsValue): JsResult[Map[Long, T]] =
    JsSuccess(jv.as[Map[String, T]].map{case (k, v) =>
      k.toString.toLong -> v .asInstanceOf[T]
    })
}

class MapLongWrites[T]()(implicit writes: Writes[T])  extends Writes[Map[Long, T]] {
  def writes(map: Map[Long, T]): JsValue =
    Json.obj(map.map{case (s, o) =>
      val ret: (String, JsValueWrapper) = s.toString -> Json.toJson(o)
      ret
    }.toSeq:_*)
}

class MapLongFormats[T]()(implicit format: Format[T]) extends Format[Map[Long, T]]{
  override def reads(json: JsValue): JsResult[Map[Long, T]] = new MapLongReads[T].reads(json)
  override def writes(o: Map[Long, T]): JsValue = new MapLongWrites[T].writes(o)
}
like image 2
ozma Avatar answered Oct 23 '22 13:10

ozma