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}