Noise free JSON format for sealed traits with Play 2.2 library

前端 未结 4 1937
醉梦人生
醉梦人生 2020-12-24 12:16

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 cas

相关标签:
4条回答
  • 2020-12-24 12:28

    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 answer There 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 Foo
    case class Bar(x: Int) extends Foo
    case class Baz(s: String) extends Foo
    case 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`
    
    0 讨论(0)
  • 2020-12-24 12:32

    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.

    0 讨论(0)
  • 2020-12-24 12:49

    Play 2.7

    sealed traits are supported in play-json.

    object Foo{
      implicit val format = Json.format[Foo]
    }
    

    Play 2.6

    This can be done now elegantly with play-json-derived-codecs

    Just add this:

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

    See here for the whole example: ScalaFiddle

    0 讨论(0)
  • 2020-12-24 12:51

    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
    }
    
    0 讨论(0)
提交回复
热议问题