在Play JSON的 JsObject中,对所有键名包含给定子串的键所对应的值进行递归替换
我在处理一段使用 Play JSON 的代码。它目前正在对具有已知结构的JSON做一些简单的密码掩码,以用于日志记录。
例如,给定下面的JSON:
{
"a": 1,
"password": "$uper$ecure",
"nested": {
"airShieldPassword": "12345",
"b": "abc"
}
}
结果将是:
{
"a": 1,
"password": "****",
"nested": {
"airShieldPassword": "****",
"b": "abc"
}
}
目前,这个是通过一个简单的自定义递归函数来实现的:
import play.api.libs.json._
/** Mask out each string value with "password" in its key */
private def maskPasswords(jsObject: JsObject) = {
def recursiveMask(v: JsObject): JsObject = JsObject(v.fields.map { case (k, v) =>
k -> (v match {
case JsString(_) if k.toLowerCase.contains("password") => JsString("****")
case o: JsObject => recursiveMask(o)
case value => value
})
})
recursiveMask(jsObject)
}
这不是尾递归的(正如把 @scala.annotation.tailrec 加到 recursiveMask 上所示),不过它大概不需要成为尾递归,因为它并不是一个很大的JSON树。
也就是说,我在想,是否有更好或更简单的方式来做这件事。也许Play JSON库内置了一种方法,可以替换所有JSON对象条目中满足给定条件的值。或者也许还有一个我没想到的简单尾递归解法。
要以一种更好的方式执行这个期望的掩码操作,将名称中包含 "password" 的字符串条目的值替换为 "****",应该怎么做?
解决方案
据我所知,没有直接可用的内置功能。Play JSON的变换器(JsPath.json.update、Reads[JsObject] 的组合)面向已知的路径,因此不适用于“在任何满足此谓词的位置进行改写”的情形。对于通用的深层重写,一个小型的递归遍历器是惯用的答案。
你不能以任何有用的方式将其变为尾递归——这是树形递归(每个对象递归到N 个子对象),而不是线性递归。为了满足 @tailrec 而转换为显式栈将更慢且可读性更差,且递归深度受JSON嵌套深度限制(通常小于10),因此栈溢出的担心并不大,正如你在问题中所提到的那样。
总的来说,我认为你当前的方法本来就很有道理。我唯一的建议是直接对 JsValue 进行模式匹配,这样也会同时匹配到 JsArray(当前代码会默默跳过嵌套在数组中的密码——以防这会带来问题):
private def maskPasswords(json: JsValue): JsValue = json match {
case JsObject(fields) =>
JsObject(fields.map {
case (k, _: JsString) if k.toLowerCase.contains("password") =>
k -> JsString("****")
case (k, v) =>
k -> maskPasswords(v)
})
case JsArray(values) => JsArray(values.map(maskPasswords))
case other => other
}
如果入口点确实必须是 JsObject => JsObject,你可以把调用包裹起来:
private def maskPasswords(obj: JsObject): JsObject =
maskPasswordsValue(obj).as[JsObject]
你可以在Scastie上试试这段代码 这里。