【问题标题】:Play JSON: reading optional nested properties播放 JSON:读取可选的嵌套属性
【发布时间】:2015-08-02 07:42:24
【问题描述】:

我有以下案例类和 JSON 组合器:

case class Commit(
    sha: String,
    username: String,
    message: String
)

object Commit {
    implicit val format = Json.format[Commit]
}

case class Build(
    projectName: String,
    parentNumber: String,
    commits: List[Commit]
)

val buildReads: Reads[Build] =
    for {
        projectName <- (__ \ "buildType" \ "projectName").read[String]
        name <- (__ \ "buildType" \ "name").read[String]
        parentNumber <- ((__ \ "artifact-dependencies" \ "build")(0) \ "number").read[String]
        changes <- (__ \ "changes" \ "change").read[List[Map[String, String]]]
    } yield {
        val commits = for {
            change <- changes
            sha <- change.get("version")
            username <- change.get("username")
            comment <- change.get("comment")
        } yield Commit(sha, username, comment)
        Build(s"$projectName::$name", parentNumber, commits)
    }

Build 的 JSON 读取组合器将处理传入的 JSON,例如:

{
    "buildType": {
        "projectName": "foo",
        "name": "bar"
    },
    "artifact-dependencies": {
        "build": [{
            "number": "1"
        }]
    },
    "changes": {
        "change": [{
            "verison": "1",
            "username": "bob",
            "comment": "foo"
        }]
    }
}

但是,如果artifact-dependencies 丢失,它会倒下。我希望这是可选的。

我应该使用readNullable吗?我试过这样做,但失败了,因为它是一个嵌套属性。

这看起来很实用,还是我滥用 JSON 组合器将我的 JSON 解析为案例类?

【问题讨论】:

  • 突然冒出来:你能用.asOpt代替.read吗?
  • 这在JsPath 对象上不可用。
  • 您能粘贴一些示例 json 吗?我的感觉是你让这变得不必要地复杂了。
  • 嗨@LuxuryMode,我的原始帖子中已经有示例JSON?
  • @OliverJosephAsh doh。不知道我是怎么错过的。

标签: json scala playframework combinators


【解决方案1】:

目前未使用其伴随对象中的Format[Commit]。我们没有理由不能为此使用简单的组合器,并分离逻辑。

case class Commit(sha: String, username: String, message: String)

object Commit {

    implicit val reads: Reads[Commit] = (
        (__ \ "version").read[String] and 
        (__ \ "username").read[String] and 
        (__ \ "comment").read[String]
    )(Commit.apply _)

}

那么,如果"artifact-dependencies" 可能丢失,我们应该在Build. 中将parentNumber 设为Option[String]

 case class Build(projectName: String, parentNumber: Option[String], commits: List[Commit])

我将合并项目名称的Reads 拆分为一个单独的名称,以使Reads[Build] 看起来更干净一些。

val nameReads: Reads[String] = for {
    projectName <- (__ \ "projectName").read[String]
    name <- (__ \ "name").read[String]
} yield s"$projectName::$name"

然后,当"artifact-dependencies" 丢失时,我们可以使用orElseReads.pure(None) 来填充None,当整个分支(或子分支)不存在时。在这种情况下,这将比映射每个步骤更简单。

implicit val buildReads: Reads[Build] = (
    (__ \ "buildType").read[String](nameReads) and
    ((__ \ "artifact-dependencies" \ "build")(0) \ "number").readNullable[String].orElse(Reads.pure(None)) and
    (__ \ "changes" \ "change").read[List[Commit]]
)(Build.apply _)

val js2 = Json.parse("""
{
    "buildType": {
        "projectName": "foo",
        "name": "bar"
    },
    "changes": {
        "change": [{
            "version": "1",
            "username": "bob",
            "comment": "foo"
        }]
    }
}
""")

scala> js2.validate[Build]
res6: play.api.libs.json.JsResult[Build] = JsSuccess(Build(foo::bar,None,List(Commit(1,bob,foo))),)

【讨论】:

  • 谢谢!我不确定是否有一个用于验证 JSON 的类(即BuildJson)和一个具有适合我偏好的数据结构的单独类(即Build)会更好——然后我必须手动他们之间的映射。上述用例是否滥用 JSON 读取组合器来实现它们并非设计的用途?
  • 我在我的示例之外使用现有的Format[Commit] 组合器,但我想我没有理由不能定义另一个并传递它,即.read[List[Commit]](customCommitReader)?
  • 是的,对于特定的用例,您始终可以手动传递Reads。我不会说这是对图书馆的滥用。如果您无法从不同的格式读取 JSON,那么除了更改您自己的类结构之外,您无能为力。
  • 有什么想法可以更新提交阅读器,以便我可以像.read[List[Commit]](customCommitReader) 一样传递它吗?是否需要将其定义为List[Commit] 类型的阅读器?
  • 除非我遗漏了什么,否则我相信我的方法比这更干净 ;)
【解决方案2】:

我尽量让我的格式与 json 匹配。诚然,在这种情况下有点尴尬,但那是因为 json 模式有点奇怪。考虑到这些限制,我会这样做:

import play.api.libs.functional.syntax._
import play.api.libs.json._
case class Build(buildType: BuildType, `artifact-dependencies`: Option[ArtifactDependencies], changes: Changes)
case class BuildType(projectName: String, name: String)
case class ArtifactDependencies(build: List[DependencyInfo])
case class DependencyInfo(number: String)
case class Changes(change: List[Commit])
case class Commit(version: String, username: String, comment: String)

object BuildType {
  implicit val buildTypeReads: Reads[BuildType] = (
    (JsPath \ "projectName").read[String] and
    (JsPath \ "name").read[String]
  )(BuildType.apply _)

}

object ArtifactDependencies {
  implicit val artifactDependencyReads: Reads[ArtifactDependencies] =
    (JsPath \ "build").read[List[DependencyInfo]].map(ArtifactDependencies.apply)
}

object DependencyInfo {
  implicit val dependencyInfoReads: Reads[DependencyInfo] =
    (JsPath \ "number").read[String].map(DependencyInfo.apply)

}

object Changes {
  implicit val changesReads: Reads[Changes] =
    (JsPath \ "change").read[List[Commit]].map(Changes.apply)
}

object Commit {
  implicit val commitReads: Reads[Commit] = (
    (JsPath \ "version").read[String] and
    (JsPath \ "username").read[String] and
    (JsPath \ "comment").read[String]
  )(Commit.apply _)
}
object Build {

  implicit val buildReads: Reads[Build] = (
    (JsPath \ "buildType").read[BuildType] and
    (JsPath \ "artifact-dependencies").readNullable[ArtifactDependencies] and
    (JsPath \ "changes").read[Changes]
  )(Build.apply _)

  def test() = {
    val js = Json.parse(
      """
        |{
        |    "buildType": {
        |        "projectName": "foo",
        |        "name": "bar"
        |    },
        |    "changes": {
        |        "change": [{
        |            "version": "1",
        |            "username": "bob",
        |            "comment": "foo"
        |        }]
        |    }
        |}
      """.stripMargin)

    println(js.validate[Build])

    val js1 = Json.parse(
      """
        |{
        |    "buildType": {
        |        "projectName": "foo",
        |        "name": "bar"
        |    },
        |    "artifact-dependencies": {
        |        "build": [{
        |            "number": "1"
        |        }]
        |    },
        |    "changes": {
        |        "change": [{
        |            "version": "1",
        |            "username": "bob",
        |            "comment": "foo"
        |        }]
        |    }
        |}
      """.stripMargin)

    println(js1.validate[Build])
  }
}

输出是:

[info] JsSuccess(Build(BuildType(foo,bar),None,Changes(List(Commit(1,bob,foo)))),)
[info] JsSuccess(Build(BuildType(foo,bar),Some(ArtifactDependencies(List(DependencyInfo(1)))),Changes(List(Commit(1,bob,foo)))),)

注意有点别扭

(JsPath \ "change").read[List[Commit]].map(Changes.apply)

对于单参数案例类是必需的。

编辑:

我错过的关键部分是parentNumber 现在成为Build 上定义的方法,如下所示:

case class Build(buildType: BuildType, `artifact-dependencies`: Option[ArtifactDependencies], changes: Changes) {
  def parentNumber: Option[String] = `artifact-dependencies`.flatMap(_.build.headOption.map(_.number))
}

【讨论】:

  • 谢谢!您的示例与我的示例不同。 Commit 具有不同的签名,在我的示例中,我加入 projectNamename 以形成单个字符串名称。我也不希望将读取定义为隐式,因为我想将它们保存为相同的映射。
  • @OliverJosephAsh 您可以按照我在上次编辑中显示的相同方式执行 projectName 操作。我想这取决于个人喜好。我倾向于对 json 结构进行机械翻译。我认为它不太容易出错,对我来说,在普通读/写之外进行您尝试进行的转换类型是有意义的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-11-26
  • 2016-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-10
相关资源
最近更新 更多