我正在试验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]
, 但这也不能解决问题,因为我们试图使用子类型 Null of Person(。总结:没有比编写Writes[ServiceResponse[Null]]
的特定实现更短的方法来处理具有空数据字段的服务响应,它既不是超级类型也不是子类型的Write[ServiceResponse[Person]]
。(一种方法可能是工会类型,但我认为这是一种矫枉过正(。90%确定我的推理,如果我错了,请纠正我:)
为了更好地解释它,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 是字符串并且是标准类型,因此默认情况下 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]]
将以下行带入范围,将确保为可选写入 null。
implicit val config =
JsonConfiguration(optionHandlers = OptionHandlers.WritesNull)
<script src="https://scastie.scala-lang.org/shankarshastri/rCUmEqXLTeuGqRNG6PPLpQ/6.js"></script>