Play JSON formatter for Map[Int,_]
Asked Answered
C

8

12

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]?

Crosslink answered 4/12, 2014 at 3:30 Comment(0)
B
20

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.

Brocket answered 4/12, 2014 at 5:18 Comment(0)
D
8

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

Dotted answered 4/12, 2014 at 3:53 Comment(2)
If I had stopped to think for a moment I would have realized that I already knew that haha. Just a moment of me being dense, sorry. Regardless, is it possible to define Reads/Writes such that the scala object will have Map[Int,Boolean] but write a JSON object with string keys? Essentially parse integers from all of the JSON keys to make a Map[Int,_]?Crosslink
I suggest you go back and improve your question. Hopefully then someone else (who knows Play better) will answer.Dotted
E
3

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

Evania answered 21/1, 2021 at 22:54 Comment(0)
D
2

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)
}
Definitely answered 8/3, 2016 at 9:55 Comment(0)
D
1

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

Drowse answered 12/6, 2017 at 10:20 Comment(1)
You have a small typo in the FromString instance val name for UserId, preventing it from compile.Cowey
H
1

A specific KeyWrites and KeyReads is available in play-json 2.9.x

private implicit val longKeyWrites = KeyWrites[Int](_.toString)
private implicit val longKeyReads =
    KeyReads[Int](str => Try(str.toInt).fold(e => JsError(e.getMessage), JsSuccess(_)))

Json.obj("1" -> "test").validate[Map[Int,String]] // JsSuccess(Map(1 -> test))
Horton answered 31/5, 2021 at 13:47 Comment(0)
C
0

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),)
Coextend answered 22/12, 2018 at 14:25 Comment(0)
W
-1

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

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

Whitecollar answered 17/5, 2020 at 18:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.