21

I'm trying to convert Scala to JSON in the 2.1RC Play Framework.

I can do the following and get JSON:

import play.api.libs.json._

val a1=Map("val1"->"a", "val2"->"b")
Json.toJSon(a1)

Because a1 is just Map[String,String] that works OK.

But if I have something more complex like where I have Map[String,Object], that doesn't work:

val a = Map("val1" -> "xxx", "val2"-> List("a", "b", "c"))
Json.toJSon(a1)
>>> error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]

I found that I can do something like the following:

val a2 = Map("val1" -> Json.toJson("a"), "val2" -> Json.toJson(List("a", "b", "c")))
Json.toJson(a2)

And that works.

But how can I do that in a general way? I thought that I could do something like the following:

a.map{ case(k,v)=> (k, Json.toJson(v) )}
>>> error: No Json deserializer found for type Object

But I still get an error that it can't be deserialized


Additional Information:

Json.toJson can convert a Map[String, String] to a JsValue:

scala> val b = Map( "1" -> "A", "2" -> "B", "3" -> "C", "4" -> "D" )
b: scala.collection.immutable.Map[String,String] = Map(1 -> A, 2 -> B, 3 -> C, 4 -> D)

scala> Json.toJson(b)
res31: play.api.libs.json.JsValue = {"1":"A","2":"B","3":"C","4":"D"}

But, it fails in trying to convert a Map[String, Object]:

scala> a
res34: scala.collection.immutable.Map[String,Object] = Map(val1 -> xxx, val2 -> List(a, b, c))

scala> Json.toJson(a)
<console>:12: error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]. Try to implement an implicit Writes or Format for this type.
          Json.toJson(a)

Using the 'hint' from this Play Framework page on converting Scala to Json, I found the following (http://www.playframework.org/documentation/2.0.1/ScalaJson):

If instead of Map[String, Object], there is a Map[String, JsValue], then Json.toJson() will work:

scala> val c = Map("aa" -> Json.toJson("xxxx"), "bb" -> Json.toJson( List("11", "22", "33") ) )
c: scala.collection.immutable.Map[String,play.api.libs.json.JsValue] = Map(aa -> "xxxx", bb -> ["11","22","33"])

scala> Json.toJson(c)
res36: play.api.libs.json.JsValue = {"aa":"xxxx","bb":["11","22","33"]}

So, what I would like, is that given a Map[String, Object], where I know that the Object values were all originally of type String or List[String], how to apply the function Json.toJson() to the all the values in the map and get a Map[String, JsValue].

I also found that I can filter out those values that are purely string and those that are (were) of type List[String]:

scala> val a1 = a.filter({case(k,v) => v.isInstanceOf[String]})
a1: scala.collection.immutable.Map[String,Object] = Map(val1 -> xxx)

scala> val a2 = a.filter({case(k,v) => v.isInstanceOf[List[String]]})
<console>:11: warning: non-variable type argument String in type List[String] is unchecked since it is eliminated by erasure
   val a2 = a.filter({case(k,v) => v.isInstanceOf[List[String]]})
                                                 ^
a2: scala.collection.immutable.Map[String,Object] = Map(val2 -> List(a, b, c))

The List[String] filtering gives a warning, but seems to give the answer I want. If the two filters could be applied and then Json.toJson() used on the values of the result, and the results combined, maybe that would work?

But the filtered results are still of type Map[String, Object] which causes a problem:

scala> Json.toJson(a1)
<console>:13: error: No Json deserializer found for type scala.collection.immutable.Map[String,Object]. Try to implement an implicit Writes or Format for this type.
          Json.toJson(a1)

1 Answer 1

26

Play 2.1 JSON API does not provide a serializer for the Type Map[String, Ojbect].

Define case class and Format for the specific type instead of Map[String, Object]:

// { "val1" : "xxx", "val2" : ["a", "b", "c"] }
case class Hoge(val1: String, val2: List[String])

implicit val hogeFormat = Json.format[Hoge]

If you don't want to create case class. The following code provides JSON serializer/deserializer for Map[String, Object]:

implicit val objectMapFormat = new Format[Map[String, Object]] {

  def writes(map: Map[String, Object]): JsValue =
    Json.obj(
      "val1" -> map("val1").asInstanceOf[String],
      "val2" -> map("val2").asInstanceOf[List[String]]
    )

  def reads(jv: JsValue): JsResult[Map[String, Object]] =
    JsSuccess(Map("val1" -> (jv \ "val1").as[String], "val2" -> (jv \ "val2").as[List[String]]))
}

More dynamically

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

implicit val objectMapFormat = new Format[Map[String, Object]] {

  def writes(map: Map[String, Object]): JsValue = 
    Json.obj(map.map{case (s, o) =>
      val ret:(String, JsValueWrapper) = o match {
        case _:String => s -> JsString(o.asInstanceOf[String])
        case _ => s -> JsArray(o.asInstanceOf[List[String]].map(JsString(_)))
      }
      ret
    }.toSeq:_*)


  def reads(jv: JsValue): JsResult[Map[String, Object]] =
    JsSuccess(jv.as[Map[String, JsValue]].map{case (k, v) =>
      k -> (v match {
        case s:JsString => s.as[String]
        case l => l.as[List[String]]
      })
    })
}

Sample code:

  val jv = Json.toJson(Map("val1" -> "xxx", "val2" -> List("a", "b", "c"), "val3" -> "sss", "val4" -> List("d", "e", "f")))
  println(jv)
  val jr = Json.fromJson[Map[String, Object]](jv)
  println(jr.get)

The output:

> {"val1":"xxx","val2":["a","b","c"],"val3":"sss","val4":["d","e","f"]}
> Map(val1 -> xxx, val2 -> List(a, b, c), val3 -> sss, val4 -> List(d, e, f))
6
  • Thanks, but I would like to do it in a generic way. The example shows a Map with two key-value pairs. There potentially could be many pairs with the values as either String or List[String]. The idea is to convert the value of the pair to a JsValue and to then do Json.toJson(Map[String,JsValue]) which works OK. I'm trying to recreate a Map[String,JsValue] from the Map[String,Object] where I know that Object is always really of type String or List[String]. Commented Jan 23, 2013 at 4:23
  • @GeorgeHernando OK. There are some ambiguities in my understanding. Cloud you show me some example input object and expected output JSON?
    – sndyuk
    Commented Jan 23, 2013 at 13:44
  • OK. I added more information in the description about what I'm trying to do. Commented Jan 23, 2013 at 14:40
  • @GeorgeHernando Please check my sample code and the output. Is this what you want?
    – sndyuk
    Commented Jan 23, 2013 at 17:59
  • 1
    This is great and will do what I want, but I'm still not sure why a simple mapping like the following fails: val b = a.map{ case(k,v)=> (k, Json.toJson(v) )}; Json.toJson(b) Commented Jan 23, 2013 at 18:18

Not the answer you're looking for? Browse other questions tagged or ask your own question.