JSON Playback: Reading Additional Attached Properties

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 a combinator to Build

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

absent, it will fall. I would like it to be optional.

Should I use readNullable

? I tried to do this but it fails because it is a nested property.

Does this look pragmatic, or am I overusing JSON combinators to parse my JSON into a case class?

+3


source to share


2 answers


Currently Format[Commit]

not used in its companion object. There is no reason why we cannot use simple combinators for this, and we 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 absent, we should do parentNumber

a Option[String]

inBuild.

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

      



I have broken down Reads

which merges the project names into a separate one to make Reads[Build]

it cleaner.

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

      

Then, when not present "artifact-dependencies"

, we can use orElse

and Reads.pure(None)

to fill it up None

when that whole branch (or child branch) is not there. In this case, it will be easier than displaying each step along 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))),)

      

+4


source


I try to get my formats to match json as closely as possible. True, in this case it's a little awkward, but that's because the JSON schema is kind of weird. This is how I would do it, given these restrictions:

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])
  }
}

      

Output:

[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)))),)

      

Please note that slightly uncomfortable



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

      

required for argument classes with one argument.

EDIT:

The crucial part I missed is that parentNumber

now becomes a method defined Build

like this:

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

      

0


source







All Articles