Comment formatter une map en Json avec Play

Publié le 10 Octobre 2016

Play ne fournit pas par défaut de format pour sérialiser une map en Json. Bien sûr si vous utiliser une Map[String, V] et que vous avez un format implicitement disponible pour le type V alors vous pouvez sérialiser votre map sans effort supplémentaire.

 

En revanche lorsque les clés sont d'un type non-standard. Par exemple pour sérialiser une map dont les clés sont aussi une case class. Dans ce cas autant écrire un format qui supporte n'importe quel type the map: Map[K, V].

 

Pour cela on utilise une case class qui va comporter 2 champs: 1 pour la clé et un pour la valeur. Ce qui donnera le json suivant:

 

[
  {
    "key": {....},
    "value": {...}
  }, {
    "key": {...},
    "value": {....}
  },
  ...
]

 

Il faut donc définir la case class suivante avec les champs key et value:

case class Entry[K, V](key: K, value: V)

 

Ensuite il faut écrire le format qui permettra de sérialiser notre map vers/depuis json:

import play.api.libs.json._
import play.api.libs.functional.syntax._

object MapFormat {
   def format[K, V](implicit formatKey: Format[K], formatValue: Format[V]) =
     new Format[Map[K, V]] {
        // on définit un format qui permet de sérialiser notre case class Entry
        implicit def formatEntry = (
          (__ \ "key").format[K] ~
          (__ \ "value").format[V]
        )(Entry.apply, unlift(Entry.unapply[K, V]))
       
// Apparemment Json.format[Entry[K, V]] ne fonctionne pas
       // (Assertion error at runtime)


      // gère la transformation Map[K, V] vers json

     override def writes(entries: Map[K, V]): JsValue =
        // on transforme notre map en sequence d'Entry et on sérialise vers json
        Json.toJson(entries.map { case (key, value) => Entry(key, value)})

      // gère la transformtion json -> Map[K, V]
      override
def reads(json: JsValue): JsResult[Map[K, V]] =
         // parse le json en List[Entry[K, V]]
         // puis extrait les paires clé/valeur pour en faire une map

         Json.fromJson[List[Entry[K, V]]](json).map(
           _.map(entry => entry.key -> entry.value).toMap
        )
   }
}

 

L'utilisation est la suivante:

// définit les type pour les clés et les valeurs
case class MyKey(underlying: String)

case class MyValue(underlying: String)
// ainsi que les format qui vont avec
implicit val formatKey = Json.format[MyKey]
implicit val formatValue = Json.format[MyValue]
implicit val mapFormat = MapFormat.format[MyKey, MyValue]
// définit notre map
val myMap = Map(
   MyKey("1") -> MyValue("A"),
   MyKey("2") -> MyValue("B")
)
val json = Json.toJson(myMap)

Ce qui produit le json suivant:

[
  {
    "key": {
     
"underlying": "1"
   },
    "value": {
     
"underlying": "A"
   }
  }, {
    "key": {
     
"underlying""2"
   },
    "value": {
     
"underlying": "B"
   }
  }
]

Ici les types MyKey and MyValue sont intentionnellement trop simple. Mais ce qui est bien c'est que le format fonctionne avec n'importe quel type du moment qu'il y a un format disponible.

Et enfin les formats pour les types MyKey et MyValue sont normalement définis dans les companions object pour éviter des problèmes d'import.

Rédigé par Bliz

Publié dans #Scala

Repost 0
Commenter cet article