Scala Play 中的空序列化(隐式写入)

Null serialization in Scala Play (implicit Writes)

我正在试验 Scala Play,不明白为什么 Null json 序列化不能开箱即用。我写了一个简单的 class 来封装响应 (ServiceResponse) 中的数据,其中 return 一个参数化的可空字段 data 并且还隐含了一个 Writes[Null],我错过了什么?编译器建议写一个 Writes[ServiceResponse[Null]] 可以工作,但是感觉很麻烦,我想知道是否有更简洁的方法来解决这个问题。下面是代码和错误。


// <!-- EDIT -->

// The data I want to transfer

case class Person (name: String, age: Int)

object Person {

  def apply(name: String, age: Int): Person = new Person(name, age)

  implicit val reader: Reads[Person] = Json.reads[Person]
  implicit val writer: OWrites[Person] = Json.writes[Person]

}


// <!-- END EDIT -->

case class ServiceResponse[T] (data: T, errorMessage: String, debugMessage: String)

object ServiceResponse {

  def apply[T](data: T, errorMessage: String, debugMessage: String): ServiceResponse[T] =
    new ServiceResponse(data, errorMessage, debugMessage)

  implicit def NullWriter: Writes[Null] = (_: Null) => JsNull

  implicit def writer[T](implicit fmt: Writes[T]) = Json.writes[ServiceResponse[T]]

}

// <!-- EDIT -->

// Given the above code I would expect
// the call ` Json.toJson(ServiceResponse(null, "Error in deserializing json", null))`
// to produce the following Json: `{ "data": null, "errorMessage": "Error in deserializing json", "debugMessage": null }`


No Json serializer found for type models.ServiceResponse[Null]. Try to implement an implicit Writes or Format for this type.

In C:\...\EchoController.scala:19
15    val wrapAndEcho = Action(parse.json) {
16      request =>
17        request.body.validate[Person] match {
18          case JsSuccess(p, _) => Ok(Json.toJson(echoService.wrapEcho(p)))
19          case JsError(errors) => BadRequest(Json.toJson(ServiceResponse(null, 
20            "Error in deserializing json", null)))
21        }
22    }


编辑

尝试改用 Option(确实是更像 Scala 的解决方案)但代码结构相同并报告相同的错误


object ServiceResponse {

  def apply[T](data: Option[T], errorMessage: Option[String], debugMessage: Option[String]): ServiceResponse[T] =
    new ServiceResponse(data, errorMessage, debugMessage)

  implicit def optionWriter[T](implicit fmt: Writes[T]) = new Writes[Option[T]] {
    override def writes(o: Option[T]): JsValue = o match {
      case Some(value) => fmt.writes(value)
      case None => JsNull
    }
  }

  implicit def writer[T](implicit fmt: Writes[Option[T]]) = Json.writes[ServiceResponse[T]]

}

我想我需要一些结构上的改变,但我不知道是什么。还尝试将 fmt 的类型更改为 Writes[T](T 是唯一的真实变量类型)并得到一个新的错误。

diverging implicit expansion for type play.api.libs.json.Writes[T1]
[error] starting with object StringWrites in trait DefaultWrites
[error]           case JsError(errors) => BadRequest(Json.toJson(ServiceResponse(None,
...

以为我找到了一个合理的解决方案,但在隐式扩展中仍然出错:(。我真的需要一个提示。

object ServiceResponse {

  def apply[T](data: Option[T], errorMessage: Option[String], debugMessage: Option[String]): ServiceResponse[T] =
    new ServiceResponse(data, errorMessage, debugMessage)

  implicit def writer[T](implicit fmt: Writes[T]): OWrites[ServiceResponse[T]] = (o: ServiceResponse[T]) => JsObject(
    Seq(
      ("data", o.data.map(fmt.writes).getOrElse(JsNull)),
      ("errorMessage", o.errorMessage.map(JsString).getOrElse(JsNull)),
      ("debugMessage", o.debugMessage.map(JsString).getOrElse(JsNull))
    )
  )
}

注意 我想我遇到了问题。由于 null 是 Person 实例的可能值,因此我希望它由 Writes[Person] 处理。事实上,Writes 在其类型参数中被定义为不变的(它是 trait Writes[A] 而不是 trait Writes[+A]),因此 Writes[Null] 与隐式参数的定义不匹配,如果它被定义为Writes[+A](反过来违反 Liskow 替换原则是错误的;它可能是 Writes[-A],但这也不能解决问题,因为我们正在尝试使用 Person 的子类型 Null)。总结:没有比编写 Writes[ServiceResponse[Null]] 的特定实现更短的方法来处理具有空数据字段的 ServiceResponse,它既不是 Write[ServiceResponse[Person]] 的超类型也不是子类型。 (一种方法可以是联合类型,但我认为这是一种矫枉过正)。 90% 肯定我的推理,如果我错了请纠正我:)

为了更好地解释,ServiceResponse 案例 class 采用类型参数 T,它可以是任何东西。 play-json 只能为标准 scala 类型提供 JSON 格式,对于自定义类型,您需要定义 JSON 格式化程序。

import play.api.libs.json._

case class ServiceResponse[T](
    data: T,
    errorMessage: Option[String],
    debugMessage: Option[String]
)

def format[T](implicit format: Format[T]) = Json.format[ServiceResponse[T]]

implicit val formatString = format[String]

val serviceResponseStr = ServiceResponse[String]("Hello", None, None)

val serviceResponseJsValue = Json.toJson(serviceResponseStr)

val fromString =
  Json.fromJson[ServiceResponse[String]](serviceResponseJsValue)

serviceResponseJsValue
fromString

serviceResponseJsValue.toString

Json.parse(serviceResponseJsValue.toString).as[ServiceResponse[String]]

在上面的示例中,您可以看到我想创建一个数据为字符串的 ServiceResponse,因此我实现了 Json.toJson 以及 Json.fromJson 所必需的格式字符串为类型 T 实现读者和作者。由于 T 是字符串并且是标准类型,play-json 默认情况下解析相同。

我添加了 scastie 片段,这将帮助您更好地理解它,并且您可以尝试使用它。

<script src="https://scastie.scala-lang.org/shankarshastri/spqJ1FQLS7ym1vm1ugDFEA/8.js"></script>

上面的解释足以满足一个用例,其中在 None 的情况下,密钥甚至不会作为 json 的一部分出现,但问题显然需要 key: null,以防找不到数据。

import play.api.libs.json._

implicit val config =
  JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)

case class ServiceResponse[T](
    data: Option[T],
    errorMessage: Option[String],
    debugMessage: Option[String]
)

def format[T](implicit format: Format[T]) = Json.format[ServiceResponse[T]]

implicit val formatString = format[String]

val serviceResponseStr = ServiceResponse[String](None, None, None)

val serviceResponseJsValue = Json.toJson(serviceResponseStr)

val fromString =
  Json.fromJson[ServiceResponse[String]](serviceResponseJsValue)

serviceResponseJsValue
fromString

serviceResponseJsValue.toString

Json.parse(serviceResponseJsValue.toString).as[ServiceResponse[String]]

在范围内引入以下行,将确保为可选写入空值。

implicit val config =
  JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)

<script src="https://scastie.scala-lang.org/shankarshastri/rCUmEqXLTeuGqRNG6PPLpQ/6.js"></script>