Search code examples
scalaplayframeworkplay-json

scala-play 2.4.11, is it possible to desearialize Map with case class as key?


I'm trying to deal with play-json and it doesn't go well. Here are my case classes

sealed case class Items(items: List[Item])

sealed case class Item(path: String, itemCounters: Map[ItemCategory, Long])

sealed case class ItemCategory(repository: Repository)

sealed case class Repository(env: String)

Here I'm trying to parse json:

implicit lazy val repositoryFormat = Json.format[Repository]
implicit lazy val itemCategoryFormat = Json.format[ItemCategory]
implicit lazy val itemFormat = Json.format[Item]
implicit lazy val itemsFormat = Json.format[Items]

Json.parse(str).as[Items]

I get exception: No implicit format for Map[ItemCategory,Long] available.

Why?


Solution

  • It fails because play-json is confused about how to deserialize itemCounters: Map[ItemCategory, Long] property in Item.

    Indeed, JSON maps can be processed directly if the key is a String. But it becomes a bit tougher with other structured objects in keys, like ItemCategory in the question. Surely, the JSON with such key can't be { "repository": { "env": "demo" } }: 1!

    So, we need to be explicit about the deserialization of this sort of Map. I'm assuming that the key for an ItemCategory is the underlying ItemCategory.repository.env value, but it can be any other property depending on your effective data model.

    We provide a Reads implementation for this kind of map:

    implicit lazy val itemCategoryMapReads = new Reads[Map[ItemCategory, Long]] {
      override def reads(jsVal: JsValue): JsResult[Map[ItemCategory, Long]] = {
        JsSuccess(
          // the original string -> number map is translated into ItemCategory -> Long
          jsVal.as[Map[String, Long]].map{
            case (category, id) => (ItemCategory(Repository(category)), id)
          }
        )
      }
    }
    

    And the respective Format (with a stub for Writes, which we do not need right now):

    implicit lazy val itemCategoryMapFormat = Format(itemCategoryMapReads, (catMap: Map[ItemCategory, Long]) => ???)
    

    The base JSON is now mapped correctly:

    val strItemCat =
      """
        | {
        |   "rep1": 1,
        |   "rep2": 2,
        |   "rep3": 3
        | }
      """.stripMargin
    
    println(Json.parse(strItemCat).as[Map[ItemCategory, Long]])
    // Map(ItemCategory(Repository(rep1)) -> 1, ItemCategory(Repository(rep2)) -> 2, ItemCategory(Repository(rep3)) -> 3)
    

    For the other case classes, the straightforward formats that you already defined should work properly, provided that they are declared in order from most to least specific (from Repository to Items).