在Play JSON的 JsObject中,对所有键名包含给定子串的键所对应的值进行递归替换

编程语言 2026-07-09

我在处理一段使用 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.updateReads[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上试试这段代码 这里

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章