Json为游戏"验证"

Jef*_*eff 3 validation json scala playframework playframework-2.0

对于request.body上的validate方法,它将json对象的属性名称和值类型与模型定义中定义的属性名称和值类型相匹配.现在,如果我要向json对象添加一个额外的属性并尝试验证它,它将作为JsSuccess传递,而不应该.

{ 
    "Name": "Bob",
    "Age": 20,
    "Random_Field_Not_Defined_in_Models": "Test"
}
Run Code Online (Sandbox Code Playgroud)

我的人员类定义如下

case class Person(name: String, age: Int)
Run Code Online (Sandbox Code Playgroud)

mil*_*use 7

我假设你一直在使用Play提供给你的内置Reads[T]Format[T]转换器Json.reads[T],例如:

import play.api.libs.json._

val standardReads = Json.reads[Person]
Run Code Online (Sandbox Code Playgroud)

虽然这些是超级的,但如果你需要额外的验证,你将不得不定义一个自定义Reads[Person]类; 但幸运的是,我们仍然可以利用内置的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")
    }
  } 
} 
Run Code Online (Sandbox Code Playgroud)

注意我们如何首先使用,以确保我们处理可以转换为a的东西.无需在这里重新发明轮子.standardReads Person

flatMap如果我们得到一个JsErrorfrom,我们用来有效地短路转换standardReads- 即我们只checkUnwantedKeys在需要时调用.

checkUnwantedKeys只是使用的事实JsObject真的只是围绕地图的包装,所以我们可以很容易地检查针对白名单的键的名称.

请注意,你也可以flatMap使用for-comprehension来编写它,如果你需要更多的检查阶段,它会开始看起来更清晰:

for {
    p <- standardReads.reads(jsv)
    r1 <- checkUnexpectedFields(jsv, p)
    r2 <- checkSomeOtherStuff(jsv, r1)
    r3 <- checkEvenMoreStuff(jsv, r2)
} yield r3
Run Code Online (Sandbox Code Playgroud)


sih*_*hil 6

如果你想避免太多的样板文件,可以使用一点 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)
      }
    }

  }
Run Code Online (Sandbox Code Playgroud)

然后,您可以将读取实例指定为:

implicit val reads = checkedReads(Json.reads[Person])
Run Code Online (Sandbox Code Playgroud)

这利用了相当多的 Scala 类型魔法和反射库(它可以让你查看类上的字段)。

classFields方法不依赖于一组固定的字段,而是为案例类(类型 param T)动态获取所有字段。它查看所有成员并仅收集案例类访问器(否则我们会选择像 那样的方法toString)。它返回一个Set[String]字段名称。

您会注意到checkedReads需要一个隐式的TypeTag[T]. 这是由编译器在编译时提供并由typeOf方法使用。

剩下的代码是不言自明的。如果传入的 json 与我们的第一个案例匹配(它是 aJsObject并且案例类中没有字段),那么我们返回 a JsError。否则,我们将其传递给底层读者。