【问题标题】:Json "Validate" for PlayJson“验证”播放
【发布时间】:2016-07-05 20:50:35
【问题描述】:

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

{ 
    "Name": "Bob",
    "Age": 20,
    "Random_Field_Not_Defined_in_Models": "Test"
}

我的 Person 类定义如下

case class Person(name: String, age: Int)

【问题讨论】:

  • 如您所见,附加字段不会阻止 JsSuccess 结果。就是这样。

标签: json scala validation playframework playframework-2.0


【解决方案1】:

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

import play.api.libs.json._

val standardReads = Json.reads[Person]

虽然这些超级很方便,但如果您需要附加验证,您必须定义一个自定义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")
    }
  } 
} 

注意我们如何利用standardReads first,以确保我们正在处理可以转换为@987654329的东西@。无需在这里重新发明轮子。

如果我们从standardReads 获得JsError,我们使用flatMap 来有效地缩短转换 - 即我们只在需要时调用checkUnwantedKeys

checkUnwantedKeys 只是利用JsObjectreally just a wrapper around a Map 的事实,因此我们可以轻松地对照白名单检查密钥的名称。

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

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

【讨论】:

    【解决方案2】:

    如果您想避免过多的样板,可以使用一点 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 类型魔法和反射库(让您查看类中的字段)。

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

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

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

    【讨论】:

    • 如果案例类具有 json 中不存在的可选字段或具有默认值的字段,则看起来这将失败。
    • 你能举个例子@AlexITC吗?
    • 运行这个例子,它试图反序列化一个具有默认值字段的案例类,它失败了JsError(List((/value,List(JsonValidationError(List(error.path.missing),WrappedArray()))))),问题是你的代码中的这部分if (fields.keySet -- caseClassFields).nonEmpty,它期望所有类字段在 JSON 中,否则,它返回一个包含缺失键的非空集,请参阅gist.github.com/AlexITC/2a0687b4e7d9d4fc8db53acc4b2e7fbf
    • 抱歉@AlexITC 回复缓慢。此答案显示了一种防止 JSON 解码额外字段的方法,它不会以您想要的方式更改 Reads。我已经修改了您的代码(请参阅gist.github.com/sihil/fe48b8a9a3d25b730203abd9ae2894f6)以表明您正在达到底层读取实现的行为。 Play JSON 库可以通过提供支持它的底层Reads 来读取默认值(如图所示)。你可能想看看stackoverflow.com/questions/44459027/…
    猜你喜欢
    • 2014-07-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-03
    • 2013-10-16
    相关资源
    最近更新 更多