播放 JSON:读取可选的嵌套属性
Play JSON: reading optional nested properties
我有以下案例 classes 和 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)
}
我的 JSON 读取 Build
的组合器将处理传入的 JSON 例如:
{
"buildType": {
"projectName": "foo",
"name": "bar"
},
"artifact-dependencies": {
"build": [{
"number": "1"
}]
},
"changes": {
"change": [{
"verison": "1",
"username": "bob",
"comment": "foo"
}]
}
}
不过,如果artifact-dependencies
少了,就翻车了。我希望这是可选的。
我应该使用 readNullable
吗?我试过这样做,但是失败了,因为它是嵌套的 属性.
这看起来实用吗,还是我在滥用 JSON 组合器将我的 JSON 解析为一个案例 class?
目前未使用其伴随对象中的 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"
缺失时,我们可以使用 orElse
和 Reads.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 匹配。不可否认,在这种情况下它有点尴尬,但那是因为 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))
}
我有以下案例 classes 和 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)
}
我的 JSON 读取 Build
的组合器将处理传入的 JSON 例如:
{
"buildType": {
"projectName": "foo",
"name": "bar"
},
"artifact-dependencies": {
"build": [{
"number": "1"
}]
},
"changes": {
"change": [{
"verison": "1",
"username": "bob",
"comment": "foo"
}]
}
}
不过,如果artifact-dependencies
少了,就翻车了。我希望这是可选的。
我应该使用 readNullable
吗?我试过这样做,但是失败了,因为它是嵌套的 属性.
这看起来实用吗,还是我在滥用 JSON 组合器将我的 JSON 解析为一个案例 class?
目前未使用其伴随对象中的 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"
缺失时,我们可以使用 orElse
和 Reads.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 匹配。不可否认,在这种情况下它有点尴尬,但那是因为 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))
}