【问题标题】:Null serialization in Scala Play (implicit Writes)Scala Play 中的空序列化(隐式写入)
【发布时间】:2020-09-27 07:43:16
【问题描述】:

我正在尝试使用 Scala Play,但不明白为什么 Null json 序列化不能开箱即用。我编写了一个简单的类来封装响应中的数据(ServiceResponse),它返回一个参数化的可为空字段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% 肯定我的推理,如果我错了,请纠正我:)

【问题讨论】:

  • 你为什么要使用Null 它几乎没有任何意义
  • 使用 Option[T] 作为 JSON 模型的一部分构建它。它可以缓解事情。它会看起来很整洁。
  • @cchantep Null 是 null 文字的类型。在我看来听起来不错。
  • Null 是一种解决方法类型,而不是一种健全的类型,特别是在使用类型类时

标签: json scala serialization playframework implicit


【解决方案1】:

为了更好地解释它,ServiceResponse 案例类接受一个类型参数 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 是 String 并且是标准类型,因此 play-json 默认解析相同。

我添加了 scastie sn-p,它可以帮助您更好地理解相同的内容,并且您可以玩弄相同的内容。

&lt;script src="https://scastie.scala-lang.org/shankarshastri/spqJ1FQLS7ym1vm1ugDFEA/8.js"&gt;&lt;/script&gt;

上面的解释足以满足一个用例,其中在 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)

&lt;script src="https://scastie.scala-lang.org/shankarshastri/rCUmEqXLTeuGqRNG6PPLpQ/6.js"&gt;&lt;/script&gt;

【讨论】:

  • 感谢您抽出宝贵的时间来回答我,但还是没有抓住重点:T 的实际值不是标准的 Scala 类型(是我的类 Person),它具有 Writes[Person] 实现(implicit val writer: OWrites[Person] = Json.writes[Person],在 Person 对象中)仍然无法处理空值,即使使用临时编写器 (NullWriter: Writes[Null] = (_: Null) =&gt; JsNull) 也是如此。重新表述问题是否有比手动编写 Writes[ServiceResponse[Null]] 的实现更简洁的方法来处理 ServiceResponse[Null] 序列化,因为 Person >: Null?
  • @ilmirons,所以你的意思是你有一个数据不存在的情况?你能解释一下你期望的Json吗,添加它会帮助我思考,还请添加你想要的案例类,并与我写的上面的案例类进行比较。
  • @Shankar-Sastri:做到了。请注意,我还在问题末尾添加了一条注释,我想我明白了为什么它不能像我一样工作
  • @ilmirons,所以你的意思是,你想在数据的 json 响应中有 null 是你的意思吗?
  • 是的。在最后一个 sn-p 中将data 作为 Option[T] 并将 None 序列化为 null 会更好(问题保持不变)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-05
  • 2014-02-13
  • 2016-10-24
  • 2015-07-20
  • 2017-04-11
相关资源
最近更新 更多