Noise free JSON format for sealed traits with Play 2.2 library
Asked Answered
A

4

48

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.

Aggi answered 10/6, 2013 at 10:33 Comment(10)
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.Benevolent
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.Espalier
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.Aggi
The automatic Json.format doesn't seem possible with traits, but you can write them: #14145932 ; also, I've stumbled across this question, which could be of interest for you: #6891893Benevolent
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: scala-lang.org/api/current/…. I don't know of any other Json library that does this, yet...Alcazar
@Alcazar do you mind to share that code?Aggi
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.Alcazar
@Alcazar 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.Aggi
@0__, depending on your goals, maybe we can combine forcesAlcazar
Yes sure, this is the project -- indeed, I would think that this should be pulled into play-json eventually.Aggi
A
25

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.

Aggi answered 17/6, 2013 at 10:28 Comment(3)
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.Hummingbird
This is the same issue I am having... however the sample code in the answer fails for me... is this still the preferred approach?Divisible
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]Imbrue
T
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`
Tavish answered 16/12, 2013 at 16:57 Comment(8)
Thanks for the new answer. I don't understand why the author basically rewrote what I did, 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)Aggi
Most likely he didn't know about it ... just like me :)Tavish
you wouldn't know of a library which creates formats with default values for missing properties (see #20617177 for details)Tavish
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)Aggi
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 ...Tavish
knownDirectSubclasses is broken, see 16) @ docs.scala-lang.org/overviews/macros/…Impanel
The play-json-extra link is broken.Exonerate
@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 repoTavish
A
25

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.

Aggi answered 17/6, 2013 at 10:28 Comment(3)
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.Hummingbird
This is the same issue I am having... however the sample code in the answer fails for me... is this still the preferred approach?Divisible
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]Imbrue
P
7

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

Pepito answered 18/9, 2018 at 7:4 Comment(0)
D
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
}
Diacritical answered 20/10, 2017 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.