Play JSON formatter for Map[Int,_]

ぃ、小莉子 提交于 2019-12-03 03:02:47
pichsenmeister

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.

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

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)
}

We can generalize the solution of 3x14159265 and Seth Tisue thanks to 2 small type classes:

import play.api.libs.json.Json.JsValueWrapper
import play.api.libs.json._
import simulacrum._

object MapFormat {

  @typeclass trait ToString[A] {
    def toStringValue(v: A): String
  }
  @typeclass trait FromString[A] {
    def fromString(v: String): A
  }

  implicit final def mapReads[K: FromString, V: Reads]: Reads[Map[K, V]] = 
    new Reads[Map[K, V]] {
      def reads(js: JsValue): JsResult[Map[K, V]] =
        JsSuccess(js.as[Map[String, V]].map { case (k, v) => FromString[K].fromString(k) -> v })
    }

  implicit final def mapWrites[K: ToString, V: Writes]: Writes[Map[K, V]] = 
    new Writes[Map[K, V]] {
      def writes(map: Map[K, V]): JsValue =
        Json.obj(map.map {
          case (s, o) =>
            val ret: (String, JsValueWrapper) = ToString[K].toStringValue(s) -> o
            ret
        }.toSeq: _*)
    }

  implicit final def mapFormat[K: ToString: FromString, V: Format]: Format[Map[K, V]] = Format(mapReads, mapWrites)

}

Note that I use Simulacrum (https://github.com/mpilquist/simulacrum) to define my type classes.

Here is an example of how to use it:

final case class UserId(value: String) extends AnyVal

object UserId {
  import MapFormat._

  implicit final val userToString: ToString[UserId] = 
    new ToString[UserId] {
      def toStringValue(v: UserId): String = v.value
    }

  implicit final val userFromString: FromString[UserId] = 
    new FromString[UserId] {
      def fromString(v: String): UserId = UserId(v)
    }
}

object MyApp extends App {

  import MapFormat._

  val myMap: Map[UserId, Something] = Map(...)

  Json.toJson(myMap)
}

if IntelliJ says that your import MapFormat._ is never used, you can and this: implicitly[Format[Map[UserId, Something]]] just below the import. It'll fix the pb. ;)

Like the accepted answer - a bit shorter:

implicit val mapReads: Reads[Map[Int, Boolean]] = (jv: JsValue) =>
    JsSuccess(jv.as[Map[String, Boolean]].map { case (k, v) =>
      k.toInt -> v
    })

implicit val mapWrites: Writes[Map[Int, Boolean]] = (map: Map[Int, Boolean]) =>
    Json.toJson(map.map { case (s, o) =>
     s.toString -> o
    })

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

Here a little test:

val json = Json.toJson(Map(1 -> true, 2 -> false))        
println(json) // {"1":true,"2":false}
println(json.validate[Map[Int, Boolean]]) // JsSuccess(Map(1 -> true, 2 -> false),)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!