Niko
Niko

Reputation: 800

Play/Scala: Map gets serialized as an array

I have a KG class defined like so (case class and companion object):

case class KG(a: Resource,
              b: String,
              c: Option[Resource],
              d: Option[String],
              d: Option[Seq[String]])

object KG {
  implicit val writes: Writes[KG] = (o: KG) => Json.obj(
    "a" -> o.resource.getURI,
    "b" -> o.label,
    "c" -> o.subClassOf.map(_.getURI),
    "d" -> o.d
  )
}

This is the output of my sbt console when trying to figure out what was happening

test: KG = KG(http://some-url-this-is,test,Some(http://another-url-this-is),None,None)

scala> val ontology = Seq(test)
ontology: Seq[KG] = List(KG(http://some-url-this-is,test,Some(http://another-url-this-is),None,None))

scala> val initial = ontology.groupBy(_.c.map(_.getLocalName))
initial: scala.collection.immutable.Map[Option[String],Seq[KG]] = Map(Some(Person) -> List(KG(http://some-url-this-is,test,Some(http://another-url-this-is),None,None))

scala> initial.getClass
res8: Class[_ <: scala.collection.immutable.Map[Option[String],Seq[KG]]] = class scala.collection.immutable.Map$Map1

scala> Json.toJson(initial)
res7: play.api.libs.json.JsValue = [["another-url-this-is",[{"a":"http://some-url-this-is","b":"test","c":"http://another-url-this-is","d":null}]]]

While initial is a Map, it is serialized as an array.... Why is this? Have I misconfigured any serialization implicits?

[
  [
    "another-url-this-is", 
    [
      {
        "d": null, 
        "b": "test", 
        "a": "http://some-url-this-is", 
        "c": "http://another-url-this-is"
      }
    ]
  ]
]

Upvotes: 1

Views: 228

Answers (2)

Levi Ramsey
Levi Ramsey

Reputation: 20561

The short answer is that the only Writes[Map[_,_]] Play JSON provides is for Map[String, V], as those are guaranteed to be serializable as a JSON object. For all other key types, the fact that a Map[K,V] is an Iterable[(K, V)] (if you have some experience with a Lisp, this is effectively like an "association list": a list of pairs) means that the default Writes[Iterable[(K, V)]] takes effect. This serializes each (K, V) as a JSON array of length 2 with the first value being the K and the second the V; all of these JSON arrays are then values in an outer JSON array.

This behavior arises from the fact that while the values in a JSON object can be any JSON value, the keys must be strings. In the particular case of Option[String], this doesn't work since there is no JSON string that one could use to represent None that wouldn't collide with a Some containing that string.

Play-JSON 2.8 does introduce KeyWrites typeclasses to serialize a K to a JsString. In the particular case of Option[String], though, this may not be of much value as you'd still need to choose a string: if you can be sure that that string will never occur, you could have a KeyWrites that blows up with an exception if you try to serialize the colliding string. On the KeyReads side, though, this might lead to some tricky bugs if you get a JSON payload with that string as a key.

Niko's solution for getting a Map[String,V] is excellent; this answer is going into more detail on the why. You might (if using Play-JSON 2.8) also consider replacing the Option[String] with a more meaningful constrained type and then defining a KeyWrites for that type.

Upvotes: 2

Niko
Niko

Reputation: 800

I have no idea why but the problem here is that the Map is of type Map[Option[String],Seq[KG]]. If the groupBy is configured in order to retrieve Strings for keys - instead of Option[String]s - then the serialization is performed successfully and the Json object represents actually a map. So if the code changes to the following:

val theMap: Map[String, Seq[KG]] = ontology.groupBy(_.c match {
        case Some(someActualString) => someActualString.getLocalName
        case None => "SomethingEmpty"
      })

the resulting Json is the following:

{
  "someKey": [
    {
      "d": null, 
      "b": "person", 
      "a": "http://someURL", 
      "c": "http://someOtherURL"
    }
  ]
}

Upvotes: 1

Related Questions