web-dev-qa-db-fra.com

Format JSON sans bruit pour les traits scellés avec la bibliothèque Play 2.2

J'ai besoin d'une solution de sérialisation JSON simple avec un minimum de cérémonie. J'étais donc assez heureux de trouver cette future bibliothèque Play 2.2 . Cela fonctionne parfaitement avec les classes simples, par exemple.

import play.api.libs.json._

sealed trait Foo
case class Bar(i: Int) extends Foo
case class Baz(f: Float) extends Foo

implicit val barFmt = Json.format[Bar]
implicit val bazFmt = Json.format[Baz]

Mais les échecs suivants:

implicit val fooFmt = Json.format[Foo]   // "No unapply function found"

Comment pourrais-je configurer l'extracteur manquant supposé pour Foo?

Ou recommanderiez-vous une autre bibliothèque autonome qui traite mon cas plus ou moins automatiquement? Peu m'importe que ce soit avec des macros au moment de la compilation ou une réflexion au moment de l'exécution, du moment que cela fonctionne immédiatement.

42
0__

Voici une implémentation manuelle de l'objet compagnon Foo:

implicit val barFmt = Json.format[Bar]
implicit val bazFmt = Json.format[Baz]

object Foo {
  def unapply(foo: Foo): Option[(String, JsValue)] = {
    val (prod: Product, sub) = foo match {
      case b: Bar => (b, Json.toJson(b)(barFmt))
      case b: Baz => (b, Json.toJson(b)(bazFmt))
    }
    Some(prod.productPrefix -> sub)
  }

  def apply(`class`: String, data: JsValue): Foo = {
    (`class` match {
      case "Bar" => Json.fromJson[Bar](data)(barFmt)
      case "Baz" => Json.fromJson[Baz](data)(bazFmt)
    }).get
  }
}
sealed trait Foo
case class Bar(i: Int  ) extends Foo
case class Baz(f: Float) extends Foo

implicit val fooFmt = Json.format[Foo]   // ça marche!

Vérification:

val in: Foo = Bar(33)
val js  = Json.toJson(in)
println(Json.prettyPrint(js))

val out = Json.fromJson[Foo](js).getOrElse(sys.error("Oh no!"))
assert(in == out)

Alternativement, la définition directe du format:

implicit val fooFmt: Format[Foo] = new Format[Foo] {
  def reads(json: JsValue): JsResult[Foo] = json match {
    case JsObject(Seq(("class", JsString(name)), ("data", data))) =>
      name match {
        case "Bar"  => Json.fromJson[Bar](data)(barFmt)
        case "Baz"  => Json.fromJson[Baz](data)(bazFmt)
        case _      => JsError(s"Unknown class '$name'")
      }

    case _ => JsError(s"Unexpected JSON value $json")
  }

  def writes(foo: Foo): JsValue = {
    val (prod: Product, sub) = foo match {
      case b: Bar => (b, Json.toJson(b)(barFmt))
      case b: Baz => (b, Json.toJson(b)(bazFmt))
    }
    JsObject(Seq("class" -> JsString(prod.productPrefix), "data" -> sub))
  }
}

Maintenant, idéalement, j'aimerais générer automatiquement les méthodes apply et unapply. Il semble que j'aurai besoin d'utiliser la réflexion ou de plonger dans les macros.

23
0__

MODIFIÉ 2015-09-22

La bibliothèque play-json-extra inclut la stratégie play-json-variants , mais aussi la stratégie [play-json-extensions] (chaîne plate pour les objets case mélangés avec des objets pour les classes case sans extra $ variante ou $ type sauf si nécessaire). Il fournit également des sérialiseurs et desséralisateurs pour les enums basés sur macramé .

Réponse précédente Il existe maintenant une bibliothèque appelée play-json-variants qui vous permet d'écrire:

implicit val format: Format[Foo] = Variants.format[Foo]

Cela générera automatiquement les formats correspondants, il gérera également la ambiguïté du cas suivant en ajoutant un attribut $ variant (l'équivalent de l'attribut class de 0__)

sealed trait Foo
case class Bar(x: Int) extends Foo
case class Baz(s: String) extends Foo
case class Bah(s: String) extends Foo

générerait 

val bahJson = Json.obj("s" -> "hello", "$variant" -> "Bah") // This is a `Bah`
val bazJson = Json.obj("s" -> "bye", "$variant" -> "Baz") // This is a `Baz`
val barJson = Json.obj("x" -> "42", "$variant" -> "Bar") // And this is a `Bar`
22
Jean

Un petit correctif pour la réponse précédente de 0__ concernant la définition de format direct - la méthode reads n'a pas fonctionné, et voici mon refactor, pour devenir aussi plus idiomatique -

def reads(json: JsValue): JsResult[Foo] = {

  def from(name: String, data: JsObject): JsResult[Foo] = name match {
    case "Bar"  => Json.fromJson[Bar](data)(barFmt)
    case "Baz"  => Json.fromJson[Baz](data)(bazFmt)
    case _ => JsError(s"Unknown class '$name'")
  }

  for {
    name <- (json \ "class").validate[String]
    data <- (json \ "data").validate[JsObject]
    result <- from(name, data)
  } yield result
}
3
ori danus

Lecture 2.6

Cela peut être fait maintenant avec élégance avec codecs dérivés de play-json

Ajoutez simplement ceci:

object Foo{
    implicit val jsonFormat: OFormat[Foo] = derived.oformat[Foo]()
}

Voir ici pour l'exemple complet: ScalaFiddle

1
pme