Json "Validate" 播放
Json "Validate" for Play
对于 request.body 上的验证方法,它将 json 对象的属性名称和值类型与模型定义中定义的相匹配。现在,如果我要向 json 对象添加一个额外的属性并尝试对其进行验证,它会在不应该的情况下作为 JsSuccess 传递。
{
"Name": "Bob",
"Age": 20,
"Random_Field_Not_Defined_in_Models": "Test"
}
我的人Class定义如下
case class Person(name: String, age: Int)
我假设您一直在使用 Play 通过 Json.reads[T]
为您提供的内置 Reads[T]
或 Format[T]
转换器,例如:
import play.api.libs.json._
val standardReads = Json.reads[Person]
虽然这些超级-方便,但如果您需要附加验证,则必须定义一个自定义Reads[Person]
class;但幸运的是,我们仍然可以利用内置的 JSON-to-case-class 宏来进行基本的检查和转换,然后在一切正常的情况下添加额外的自定义检查层:
val standardReads = Json.reads[Person]
val strictReads = new Reads[Person] {
val expectedKeys = Set("name", "age")
def reads(jsv:JsValue):JsResult[Person] = {
standardReads.reads(jsv).flatMap { person =>
checkUnwantedKeys(jsv, person)
}
}
private def checkUnwantedKeys(jsv:JsValue, p:Person):JsResult[Person] = {
val obj = jsv.asInstanceOf[JsObject]
val keys = obj.keys
val unwanted = keys.diff(expectedKeys)
if (unwanted.isEmpty) {
JsSuccess(p)
} else {
JsError(s"Keys: ${unwanted.mkString(",")} found in the incoming JSON")
}
}
}
请注意我们如何利用 standardReads
首先,以确保我们处理的事情 可以转换为Person
。无需在这里重新发明轮子。
如果我们从 standardReads
得到 JsError
,我们使用 flatMap
来有效地短路转换 - 即我们只在需要时调用 checkUnwantedKeys
。
checkUnwantedKeys
只是利用了 JsObject
是 really just a wrapper around a Map 的事实,所以我们可以很容易地检查键名与 a白名单。
注意 你也可以使用 for-comprehension 来写 flatMap
,如果你需要更多的检查阶段,它看起来会更清晰:
for {
p <- standardReads.reads(jsv)
r1 <- checkUnexpectedFields(jsv, p)
r2 <- checkSomeOtherStuff(jsv, r1)
r3 <- checkEvenMoreStuff(jsv, r2)
} yield r3
如果您想避免过多的样板文件,可以使用一点 Scala 反射来制作更通用的解决方案:
import play.api.libs.json._
import scala.reflect.runtime.universe._
def checkedReads[T](underlyingReads: Reads[T])(implicit typeTag: TypeTag[T]): Reads[T] = new Reads[T] {
def classFields[T: TypeTag]: Set[String] = typeOf[T].members.collect {
case m: MethodSymbol if m.isCaseAccessor => m.name.decodedName.toString
}.toSet
def reads(json: JsValue): JsResult[T] = {
val caseClassFields = classFields[T]
json match {
case JsObject(fields) if (fields.keySet -- caseClassFields).nonEmpty =>
JsError(s"Unexpected fields provided: ${(fields.keySet -- caseClassFields).mkString(", ")}")
case _ => underlyingReads.reads(json)
}
}
}
然后您可以将读取实例指定为:
implicit val reads = checkedReads(Json.reads[Person])
这利用了相当多的 Scala 类型魔法和反射库(让您可以查看 classes 上的字段)。
classFields
方法不是依赖一组固定的字段,而是动态获取案例 class 的所有字段(类型参数 T
)。它查看所有成员并仅收集大小写 class 访问器(否则我们会选择 toString
之类的方法)。它 return 是一个 Set[String]
字段名称。
您会注意到 checkedReads
采用隐式 TypeTag[T]
。这由编译器在编译时提供并由 typeOf
方法使用。
剩下的代码是不言自明的。如果传入的 json 匹配我们的第一个案例(它是一个 JsObject
并且有一些字段不在案例 class 上)那么我们 return 一个 JsError
。否则我们将其传递给底层 reader.
对于 request.body 上的验证方法,它将 json 对象的属性名称和值类型与模型定义中定义的相匹配。现在,如果我要向 json 对象添加一个额外的属性并尝试对其进行验证,它会在不应该的情况下作为 JsSuccess 传递。
{
"Name": "Bob",
"Age": 20,
"Random_Field_Not_Defined_in_Models": "Test"
}
我的人Class定义如下
case class Person(name: String, age: Int)
我假设您一直在使用 Play 通过 Json.reads[T]
为您提供的内置 Reads[T]
或 Format[T]
转换器,例如:
import play.api.libs.json._
val standardReads = Json.reads[Person]
虽然这些超级-方便,但如果您需要附加验证,则必须定义一个自定义Reads[Person]
class;但幸运的是,我们仍然可以利用内置的 JSON-to-case-class 宏来进行基本的检查和转换,然后在一切正常的情况下添加额外的自定义检查层:
val standardReads = Json.reads[Person]
val strictReads = new Reads[Person] {
val expectedKeys = Set("name", "age")
def reads(jsv:JsValue):JsResult[Person] = {
standardReads.reads(jsv).flatMap { person =>
checkUnwantedKeys(jsv, person)
}
}
private def checkUnwantedKeys(jsv:JsValue, p:Person):JsResult[Person] = {
val obj = jsv.asInstanceOf[JsObject]
val keys = obj.keys
val unwanted = keys.diff(expectedKeys)
if (unwanted.isEmpty) {
JsSuccess(p)
} else {
JsError(s"Keys: ${unwanted.mkString(",")} found in the incoming JSON")
}
}
}
请注意我们如何利用 standardReads
首先,以确保我们处理的事情 可以转换为Person
。无需在这里重新发明轮子。
如果我们从 standardReads
得到 JsError
,我们使用 flatMap
来有效地短路转换 - 即我们只在需要时调用 checkUnwantedKeys
。
checkUnwantedKeys
只是利用了 JsObject
是 really just a wrapper around a Map 的事实,所以我们可以很容易地检查键名与 a白名单。
注意 你也可以使用 for-comprehension 来写 flatMap
,如果你需要更多的检查阶段,它看起来会更清晰:
for {
p <- standardReads.reads(jsv)
r1 <- checkUnexpectedFields(jsv, p)
r2 <- checkSomeOtherStuff(jsv, r1)
r3 <- checkEvenMoreStuff(jsv, r2)
} yield r3
如果您想避免过多的样板文件,可以使用一点 Scala 反射来制作更通用的解决方案:
import play.api.libs.json._
import scala.reflect.runtime.universe._
def checkedReads[T](underlyingReads: Reads[T])(implicit typeTag: TypeTag[T]): Reads[T] = new Reads[T] {
def classFields[T: TypeTag]: Set[String] = typeOf[T].members.collect {
case m: MethodSymbol if m.isCaseAccessor => m.name.decodedName.toString
}.toSet
def reads(json: JsValue): JsResult[T] = {
val caseClassFields = classFields[T]
json match {
case JsObject(fields) if (fields.keySet -- caseClassFields).nonEmpty =>
JsError(s"Unexpected fields provided: ${(fields.keySet -- caseClassFields).mkString(", ")}")
case _ => underlyingReads.reads(json)
}
}
}
然后您可以将读取实例指定为:
implicit val reads = checkedReads(Json.reads[Person])
这利用了相当多的 Scala 类型魔法和反射库(让您可以查看 classes 上的字段)。
classFields
方法不是依赖一组固定的字段,而是动态获取案例 class 的所有字段(类型参数 T
)。它查看所有成员并仅收集大小写 class 访问器(否则我们会选择 toString
之类的方法)。它 return 是一个 Set[String]
字段名称。
您会注意到 checkedReads
采用隐式 TypeTag[T]
。这由编译器在编译时提供并由 typeOf
方法使用。
剩下的代码是不言自明的。如果传入的 json 匹配我们的第一个案例(它是一个 JsObject
并且有一些字段不在案例 class 上)那么我们 return 一个 JsError
。否则我们将其传递给底层 reader.