Search code examples
jsonscalaplayframeworkplay-json

Noise free JSON format for sealed traits with Play 2.2 library


I need to get a simple JSON serialization solution with minimum ceremony. So I was quite happy finding this forthcoming Play 2.2 library. This works perfectly with plain case classes, e.g.

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]

But the following fails:

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

How would I set up the alleged missing extractor for Foo?

Or would you recommend any other standalone library that handles my case more or less fully automatically? I don't care whether that is with macros at compile time or reflection at runtime, as long as it works out of the box.


Solution

  • 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 Foo
    case class Bar(i: Int  ) extends Foo
    case class Baz(f: Float) extends Foo
    
    implicit 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.