使用Play 2.2库实现密封特质的无噪声JSON格式

49

我需要一个简单的JSON序列化解决方案,尽量少的仪式感。所以我很高兴发现这个即将推出的Play 2.2库。它可以在普通的数据类中完美使用,例如:

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]

但是以下操作失败:

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

我应该如何为 Foo 设置所谓的缺失的提取器(extractor)?

或者您是否推荐任何其他独立库,可以更或多或少地自动处理我的情况? 只要它可以开箱即用,无论是在编译时使用宏还是在运行时使用反射都可以。


1
我需要能够序列化类型类。因此,我需要一个密封特质的格式,该特质由许多案例类扩展。这应该是一个相当常见的情况。 - 0__
我最近编写了一个JSON宏,使用编译时类型信息为任何对象结构生成jackson代码。该宏能够通过反射API“knownDirectSubclasses”为密封类型的所有子类型生成匹配语句,如下所示:http://www.scala-lang.org/api/current/index.html#scala.reflect.api.Symbols$ClassSymbol。我不知道还有哪个Json库可以做到这一点... - Andy
1
@Andy,你介意分享一下那段代码吗? - 0__
如果您可以等一周,我可以考虑将其上传到GitHub。警告:它是为我们工作中的使用情况编写的,而不是通用库。 - Andy
根据你的目标,也许我们可以联合起来。 - Andy
显示剩余5条评论
4个回答

26

2015-09-22修订

play-json-extra包含了 play-json-variants 策略,也包括了 [play-json-extensions] 策略(用于 case objects 的平面字符串与 case classes 的对象混合,除非必要,否则不添加额外的 $variant 或 $type)。它还为基于 macramé 实现的枚举类型提供了序列化和反序列化功能。

之前的回答: 现在有一个名为play-json-variants的库,允许你编写:

implicit val format: Format[Foo] = Variants.format[Foo]

它将自动生成相应的格式,并通过添加$variant属性(等同于0__的class属性)来处理以下情况的消歧,

sealed trait Foo
case class Bar(x: Int) extends Foo
case class Baz(s: String) extends Foo
case class Bah(s: String) extends Foo

将产生

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`

谢谢你的新回答。我不明白为什么作者基本上重写了我所做的事情,但是好吧...我们遇到了相同的问题,即宏系统没有安全地提供“knownDirectSubclasses”(并且确认这将不会很快修复)。 - 0__
很可能他不知道这件事……就像我一样 :) - Jean
你知道有没有一个库可以为缺失的属性创建默认值格式(详见https://dev59.com/YGIj5IYBdhLWcg3wHhtX)? - Jean
你可以将其作为我的项目的功能请求。我不想总是生成默认值,但我可以想象它是一个选项,比如 AutoFormat[Foo](defaults = true) - 0__
我不希望默认值一直生成。理想情况下,应该有两个签名:一个用于默认所有可以默认的值,另一个是withDefault(key, value),它将确保键名存在并且提供的默认值具有正确的类型。当我意识到我不仅需要为密封特质派生的类,而且还需要为“普通”案例类编写此功能请求时,我开始编写此功能请求。 - Jean
显示剩余3条评论

25

这是一个Foo伴生对象的手动实现:

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!

验证:

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)

或者选择直接格式定义:

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))
  }
}

理想情况下,我希望自动生成applyunapply方法。看起来我需要使用反射或深入研究宏。



在我看来,apply/unapply方法相当危险。如果JSON类名称未耗尽(格式不正确的JSON),则get调用将失败,并且您将无法记录任何JSON错误日志。 - Felix
这正是我遇到的问题...然而答案中的示例代码对我来说失败了...这仍然是首选方法吗? - Matt
1
对我来说,它在这一行 case "Bar" => Json.fromJson[Bar](data)(barFmt) 给出了错误,因为实际的类型是 JsValue[Bar],而期望的类型是 JsValue[Foo]。 - Arun Gupta

7

Play 2.7

play-json支持sealed traits

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

Play 2.6

现在,使用play-json-derived-codecs可以轻松实现这一功能。

只需添加以下内容:

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

点击这里查看完整示例:ScalaFiddle


4

关于 0__ 先前回答的直接格式定义的小修正 - reads 方法无效,这是我对它进行重构的方式,使其更符合习惯用法 -

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
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接