Noise free JSON format for sealed traits with Play 2.2 library Noise free JSON format for sealed traits with Play 2.2 library json json

Noise free JSON format for sealed traits with Play 2.2 library


AMENDED 2015-09-22

The library play-json-extra includes the play-json-variants strategy, but also the [play-json-extensions] strategy (flat string for case objects mixed with objects for case classes no extra $variant or $type unless needed). It also provides serializers and deserializers for macramé based enums.

Previous answerThere is now a library called play-json-variants which allows you to write :

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

This will generate the corresponding formats automatically, it will also handle disambiguation of the following case by adding a $variant attribute (the equivalent of 0__ 's class attribute)

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

would generate

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`


Here is a manual implementation of the Foo companion object:

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 Foocase class Bar(i: Int  ) extends Foocase class Baz(f: Float) extends Fooimplicit val fooFmt = Json.format[Foo]   // ça marche!

Verification:

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)

Alternatively the direct format definition:

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))  }}

Now ideally I would like to automatically generate the apply and unapply methods. It seems I will need to use either reflection or dive into macros.


A small fix for the previous answer by 0__ regarding the direct format definition - the reads method didn't work, and here is my refactor to it, to also become more idiomatic -

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}