I have the following case classes and JSON combinators:
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)
}
My JSON reads combinator for Build will handle incoming JSON such as:
{
"buildType": {
"projectName": "foo",
"name": "bar"
},
"artifact-dependencies": {
"build": [{
"number": "1"
}]
},
"changes": {
"change": [{
"verison": "1",
"username": "bob",
"comment": "foo"
}]
}
}
However, if artifact-dependencies is missing, it will fall over. I would like this to be optional.
Should I use readNullable? I have tried to do so, but this fails because it is a nested property.
Does this look pragmatic, or am I abusing JSON combinators to parse my JSON into a case class?
Currently the Format[Commit] in its companion object isn't being used. There's no reason we can't use simple combinators for that, and separate the logic.
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 _)
}
Then, if "artifact-dependencies" can be missing, we should make parentNumber an Option[String] in Build.
case class Build(projectName: String, parentNumber: Option[String], commits: List[Commit])
I split the Reads that combines project names into a separate one to make the Reads[Build] look a little more clean.
val nameReads: Reads[String] = for {
projectName <- (__ \ "projectName").read[String]
name <- (__ \ "name").read[String]
} yield s"$projectName::$name"
Then, for when "artifact-dependencies" is missing, we can use orElse and Reads.pure(None) to fill it with None when that entire branch (or sub-branch) is not there. In this case, that would be simpler than mapping each step of the way.
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))),)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With