46

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.

pme
  • 11,442
  • 2
  • 35
  • 62
0__
  • 64,257
  • 16
  • 158
  • 253
  • Is there some code missing? Is the only thing defining `Foo` the `sealed trait Foo` line? What do you expect to happen, then? I suppose `Json.format` would work for regular classes if they have an `apply()` and `unapply()` method. – Carsten Jun 10 '13 at 10:39
  • Play json, as well as lift json should be ok. You see, you are trying to get a format for a trait, but almost all libraries that provide transparent serialization are based on the case classes. Just use case classes and pattern matching and you should be fine. – vitalii Jun 10 '13 at 10:39
  • 1
    I need to be able to serialize type classes. Therefore I need a format for a sealed trait which is extended by a number of case classes. Should be a fairly common scenario. – 0__ Jun 10 '13 at 11:17
  • The automatic `Json.format` doesn't seem possible with traits, but you can write them: http://stackoverflow.com/questions/14145432/play-2-1-json-serialization-for-traits ; also, I've stumbled across this question, which could be of interest for you: http://stackoverflow.com/questions/6891393/traits-and-serialization-deserialization – Carsten Jun 10 '13 at 13:35
  • I recently wrote a JSON macro that generates jackson code for any object structure using compile time type information. The macro is able to generate a match statement for all subtypes of a sealed type through the reflection api `knownDirectSubclasses`, seen here: http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$ClassSymbol. I don't know of any other Json library that does this, yet... – Andy Jun 13 '13 at 23:18
  • 1
    @Andy do you mind to share that code? – 0__ Jun 17 '13 at 10:36
  • If you can wait a week, I can see about getting it up on GitHub. Warning: It was written for our use case at work and not as generic library. – Andy Jun 17 '13 at 17:52
  • @Andy No problem. I have halfway written my own implementation right now. The `writer` already works, still some problems to fix with singleton objects in the `reader` part. – 0__ Jun 17 '13 at 17:54
  • @0__, depending on your goals, maybe we can combine forces – Andy Jun 17 '13 at 18:03
  • Yes sure, [this is the project](https://github.com/Sciss/play-json-sealed) -- indeed, I would think that this should be pulled into play-json eventually. – 0__ Jun 17 '13 at 18:10

4 Answers4

26

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`
Jean
  • 20,591
  • 5
  • 43
  • 61
  • Thanks for the new answer. I don't understand why the author basically rewrote what [I did](https://github.com/Sciss/play-json-sealed), but well… We're having the same trouble with `knownDirectSubclasses` not safely provided by the macro system (and confirmation that this will _not_ be fixed any time soon) – 0__ Dec 16 '13 at 19:01
  • Most likely he didn't know about it ... just like me :) – Jean Dec 16 '13 at 19:17
  • you wouldn't know of a library which creates formats with default values for missing properties (see http://stackoverflow.com/questions/20616677/defaults-for-missing-properties-in-play-2-json-formats for details) – Jean Dec 17 '13 at 14:12
  • you could put that as a feature request in my project. I wouldn't want to generate default values _always_, but I could imagine it to be an option, like `AutoFormat[Foo](defaults = true)` – 0__ Dec 17 '13 at 15:55
  • I wouldn't want defaults generation all the time. Ideally there would be 2 sigs : 1 for defaulting all values which can be defaulted and a withDefault(key, value) that second one would ensure the keyname exists and the provided default value has the correct type. I started writing the feature request when I realized, I need this for "normal" case classes not only for sealed traits derived ones ... – Jean Dec 17 '13 at 16:10
  • `knownDirectSubclasses` is broken, see 16) @ http://docs.scala-lang.org/overviews/macros/changelog211.html#changes-to-the-reflection-api – Stefan K. Feb 25 '15 at 14:15
  • The play-json-extra link is broken. – Brian McCutchon May 24 '17 at 21:27
  • @BrianMcCutchon thanks for the report, I fixed the link. Unfortunately the rendered site is down (reset to an apache landing page) but the doc is available inside the repo – Jean May 26 '17 at 07:06
24

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__
  • 64,257
  • 16
  • 158
  • 253
  • in my opinion the apply/unapply approach is pretty dangerous. If the json class name isn't exhausted (Malformed json) the get call will blow off and you'll have no json error logging of this. – Felix Jun 27 '16 at 04:03
  • This is the same issue I am having... however the sample code in the answer fails for me... is this still the preferred approach? – Matt Aug 27 '18 at 15:56
  • 1
    to me it's giving Error to this line `case "Bar" => Json.fromJson[Bar](data)(barFmt)` because of mismatch to Actual : JsValue[Bar] and Expected : JsValue[Foo] – Arun Gupta Oct 31 '18 at 10:10
4

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
}
ori danus
  • 51
  • 2
4

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

pme
  • 11,442
  • 2
  • 35
  • 62