Play JSON formatter for Map[Int,_]

后端 未结 6 1371
自闭症患者
自闭症患者 2020-12-29 11:41

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 s

相关标签:
6条回答
  • 2020-12-29 11:59

    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)
    }
    
    0 讨论(0)
  • 2020-12-29 12:00

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

    0 讨论(0)
  • 2020-12-29 12:02

    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),)
    
    0 讨论(0)
  • 2020-12-29 12:03

    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. ;)

    0 讨论(0)
  • 2020-12-29 12:05

    https://gist.github.com/fancellu/0bea53f1a1dda712e179892785572ce3

    Here is a way to persist a Map[NotString,...]

    0 讨论(0)
  • 2020-12-29 12:11

    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.

    0 讨论(0)
提交回复
热议问题