从 Json 获取 NPE 在读取中验证(在 Scala 中读取自定义 class)

Getting NPE from Json validate in Reads (reading custom class in Scala)

我有一种情况,我想像这样读取一个对象:

Something(first: String, second: GPID)

GPID 是 Int 的包装器(见下文)。

class 可以很好地序列化(写入)但是当我尝试反序列化(读取)然后验证时,Play 抛出 NPE。

编辑

上面发布的例子已经过简化。我正在使用的实际代码有点复杂,所以我试图创建一个简单的示例。这是我正在使用的实际对象:

case class GPInviteRequest(token: String, userId: Option[GPID] = None, email: Option[String] = None, phoneNumber: Option[GPPhoneNumber] = None)
object GPInviteRequest {
    implicit val readsInvite = Json.reads[GPInviteRequest]
    implicit val writesInvite = Json.writes[GPInviteRequest]
}

GPID 类型基本上是 Int 的包装器。所有引用的对象(GPID、GPPhoneNumber)都有自己的 Reads/Writes。在我的第一次尝试中,我得到:JsError(List((/userId/GPID,List(ValidationError(error.path.missing,WrappedArray())))))。这是因为我没有创建格式正确的 JSON,但到目前为止还不错,服务器正确报告了错误...因此,现在我编写了一个序列化然后反序列化对象的测试:

    "serialize an invite request to/from JSON" in {
        val j1 = Json.toJson(GPInviteRequest("token@ab6f7ad89ce8ff", Option(GPID(1000))))

        println(j1.toString)

        j1.validate[GPInviteRequest].isSuccess must beTrue
    }

GPID 是 Int 的包装器,但是,它的 Reads 方法如下所示:

object GPID {
    implicit val reads: Reads[GPID] = (
        (__ \ "GPID").read[GPID]
        )
...

好的,所以,现在当我 运行 测试时,输出如下:

[info] The invite service should
[error]   ! serialize an invite request to/from JSON
{"token":"token@ab6f7ad89ce8ff","userId":{"GPID":1000}}
[error]    null (JsConstraints.scala:36)

为了完整起见,错误如下:

[error]    null (JsConstraints.scala:36)
[error] play.api.libs.json.PathReads$$anonfun$at$$anonfun$apply.apply(JsConstraints.scala:36)
[error] play.api.libs.json.PathReads$$anonfun$at$$anonfun$apply.apply(JsConstraints.scala:36)
[error] play.api.libs.json.JsResult$class.flatMap(JsResult.scala:103)
[error] play.api.libs.json.JsSuccess.flatMap(JsResult.scala:9)
[error] play.api.libs.json.PathReads$$anonfun$at.apply(JsConstraints.scala:36)
[error] play.api.libs.json.PathReads$$anonfun$at.apply(JsConstraints.scala:36)
[error] play.api.libs.json.Reads$$anon.reads(Reads.scala:101)
[error] play.api.libs.json.PathReads$$anonfun$nullable$$anonfun$apply$$anonfun$apply.apply(JsConstraints.scala:65)
[error] play.api.libs.json.PathReads$$anonfun$nullable$$anonfun$apply$$anonfun$apply.apply(JsConstraints.scala:63)
[error] play.api.libs.json.JsResult$class.fold(JsResult.scala:76)
[error] play.api.libs.json.JsSuccess.fold(JsResult.scala:9)
[error] play.api.libs.json.PathReads$$anonfun$nullable$$anonfun$apply.apply(JsConstraints.scala:61)
[error] play.api.libs.json.PathReads$$anonfun$nullable$$anonfun$apply.apply(JsConstraints.scala:61)
[error] play.api.libs.json.PathReads$$anonfun$nullable.apply(JsConstraints.scala:59)
[error] play.api.libs.json.PathReads$$anonfun$nullable.apply(JsConstraints.scala:58)
[error] play.api.libs.json.Reads$$anon.reads(Reads.scala:101)
[error] play.api.libs.json.Reads$$anon$$anon.reads(Reads.scala:81)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anon.reads(Reads.scala:101)
[error] play.api.libs.json.Reads$$anon$$anon.reads(Reads.scala:81)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anon.reads(Reads.scala:101)
[error] play.api.libs.json.Reads$$anon$$anon.reads(Reads.scala:81)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anonfun$map.apply(Reads.scala:28)
[error] play.api.libs.json.Reads$$anon.reads(Reads.scala:101)
[error] play.api.libs.json.JsValue$class.validate(JsValue.scala:73)
[error] play.api.libs.json.JsObject.validate(JsValue.scala:166)
[error] application.TestInviteServices$$anonfun$$anonfun$apply$$anonfun$apply.apply$mcZ$sp(TestInviteServices.scala:31)
[error] application.TestInviteServices$$anonfun$$anonfun$apply$$anonfun$apply.apply(TestInviteServices.scala:31)
[error] application.TestInviteServices$$anonfun$$anonfun$apply$$anonfun$apply.apply(TestInviteServices.scala:31)
[error] application.TestInviteServices$$anonfun$$anonfun$apply.apply(TestInviteServices.scala:31)
[error] application.TestInviteServices$$anonfun$$anonfun$apply.apply(TestInviteServices.scala:26)

您具体遇到了什么错误?你想达到什么目的?这对我有用:

import play.api.libs.json.Json

case class Person(first: String, middle: Option[String], last: String)

object Person {
  implicit val reads = Json.reads[Person]
}

val json = """{ "first": "Michael", "middle": null, "last": "Kendra"}"""
val json2 = """{ "first": "Michael", "last": "Kendra"}"""


Json.parse(json).validate[Person]
# >> res0: play.api.libs.json.JsResult[Person] = JsSuccess(Person(Michael,None,Kendra),)

Json.parse(json2).validate[Person]
# >> res1: play.api.libs.json.JsResult[Person] = JsSuccess(Person(Michael,None,Kendra),)

问题是你对 Reads[GPID] 的递归定义。 GPID 应该包裹 Int,但 Reads 实际上并没有显示出来。 GPID.reads 包含 (__ \ "GPID).read[GPID],它依赖于范围内的隐式 Reads[GPID]。不幸的是,编译器允许 GPID.reads 自身来满足该需求,这会抛出 NullPointerException 因为它甚至在初始化之前就试图访问自己。

case class GPID(GPID: Int)

object GPID {
    implicit val reads: Reads[GPID] = (__ \ "GPID").read[GPID]
    implicit val writes: Writes[GPID] = Json.writes[GPID]
}

scala> val gpid = GPID(1234)
gpid: GPID = GPID(1234)

scala> val js = Json.toJson(gpid)
js: play.api.libs.json.JsValue = {"GPID":1234}

scala> js.validate[GPID]
java.lang.NullPointerException
...

要解决此问题,我们只需将 Reads[GPID] 修复为不可递归,这很容易。我们真的想要 read[Int],因为它就是这样包装的。然后我们mapInt变成了GPID.

case class GPID(GPID: Int)

object GPID {
    implicit val reads: Reads[GPID] = (__ \ "GPID").read[Int].map(GPID(_))
    implicit val writes: Writes[GPID] = Json.writes[GPID]
}

scala> js.validate[GPID]
res2: play.api.libs.json.JsResult[GPID] = JsSuccess(GPID(1234),/GPID) // It works!

如果我们结合您的其他代码,它现在可以工作了:

scala> val j1 = Json.toJson(GPInviteRequest("token@ab6f7ad89ce8ff", Option(GPID(1000))))
j1: play.api.libs.json.JsValue = {"token":"token@ab6f7ad89ce8ff","userId":{"GPID":1000}}

scala> j1.validate[GPInviteRequest]
res3: play.api.libs.json.JsResult[GPInviteRequest] = JsSuccess(GPInviteRequest(token@ab6f7ad89ce8ff,Some(GPID(1000)),None),)

你还没有展示它,但我感觉 GPPhoneNumber 会有同样的问题(假设它只是包装了一个 StringReads 定义在一个类似的方式),但修复将是相同的。