Play-Json Coast-to-Coast: Best Pattern for "Nesting" Transformations - json

I'm becoming a fan of the play-json coast-to-coast pattern, especially its use of combinators. I have some complex cases I'm munging together. I know there's a better way, but I'm new to the combinator approach to build up functionality.
I'd like to turn this:
{
"somearray": [
{
"field1": "value1",
"field2": "value1",
"key": "key1"
},
{
"field1": "second1",
"field2": "second1",
"key": "key2"
}
]
}
into this:
{
"someObj": {
"key1":
{
"field1": "value1",
"field2": "value1"
},
"key2":
{
"field1": "second1",
"field2": "second1"
}
]
}
I can get this to work, but I exit out of the transformation:
(__ \ "someObj").json.copyFrom(__ \ "someArray".json.pick.map {
case JsArray(arr) => {
JsObject(arr.map(a =>
a.transform(<A transform function to prune key>).map(pruned => {
((a \ "key").as[String], pruned)
}).flatMap({
case JsSuccess(result, _) => Seq(result)
case other => Nil
})
}
case other => JsNull
})
THere are some issues with this code: I know it's verbose, I know I'm assume "key" is present with a string type, and I need that flatMap to get me out of a JsResult and into some JsValue I can use to build the JsObject.
It seems like I should be able to
1) Create a sub transformation, i.e. a.transform() can be nestled in the parent transform without unpacking and repacking the json object.
Thank you.

How about this (ignoring the fact that this code still has the same issues you already mentioned)?
def elementTransform = for {
keyValue <- (__ \ 'key).json.pick // picks value for key field
obj <- (__ \ 'key).json.prune // returns object without the key field
res <- (__ \ keyValue.as[String]).json.put(obj) // uses the keyValue as a field name
} yield res
val transform = (__ \ "someObj").json.copyFrom((__ \ "someArray").json.pick[JsArray].map(
_.value.foldLeft[JsResult[JsObject]](JsSuccess(Json.obj()))(
(obj, a) =>
for {
o <- obj
field <- a.transform(elementTransform)
} yield o ++ field
).fold(_ => JsNull, identity)
))

Related

Play Framework and Scala Json, parsing for json containing JSArray and JSObject

My sample json is either with a country object
Json sample 1
"#version": "1.0",
"country": {
"#country": "US",
"day": {
"#date": "2016-02-15",
"#value": "1"
}
}
or with country array:
Json sample 2
"#version": "1.0",
"country": [{
"#country": "US",
"day": {
"#date": "2016-02-15",
"#value": "1"
}
}, {
"#country": "UK",
"day": {
"#date": "2016-02-15",
"#value": "5"
}]
}
To read the json
implicit val dayJsonReads: Reads[DayJson] = (
(JsPath \ "#date").read[DateTime](dateReads) and
((JsPath \ "#value").read[Int] orElse (JsPath \ "#value").read[String].map(_.toInt))
)(DayJson.apply _)
implicit val countryJsonReads: Reads[CountryJson] = (
(JsPath \ "#country").read[String] and
(JsPath \ "day").read[DayJson]
)(CountryJson.apply _)
implicit val newUserJsonReads: Reads[NewUserJson] = (
(JsPath \ "#version").read[String] and
(JsPath \ "country").readNullable[Seq[CountryJson]]
)(NewUserJsonParent.apply _)
The above code reads sample json 2 however fails for sample json 1. Is it possible to use readNullable to read either JS Value or JS Object or can we convert it from JS Value to JS Object. Thank you.
You can do something like this:
object NewUserJson{
implicit val newUserJsonReads: Reads[NewUserJson] = (
(JsPath \ "#version").read[String] and
(JsPath \ "country").read[JsValue].map{
case arr: JsArray => arr.as[Seq[CountryJson]]
case obj: JsObject => Seq(obj.as[CountryJson])
}
)(NewUserJson.apply _)
}
This should work for this case class:
case class NewUserJson(`#version`: String, country: Seq[CountryJson])
But I don't like it, can't you just use the same structure, and when you have only one country just send a list that hold only one country, instead of object?
Working on Tomer's solution, below is a working sample. It would be nice if I can make it more compact.
Case class
case class NewUserJson(version: String, country: Option[Seq[CountryJson]])
Json parsing object
object NewUserJson{
implicit val newUserJsonReads: Reads[NewUserJson] = (
(JsPath \ "#version").read[String] and
(JsPath \ "country").readNullable[JsValue].map {
arr => {
if (!arr.isEmpty){
arr.get match {
case arr: JsArray => Option(arr.as[Seq[CountryJson]])
case arr: JsObject => Option(Seq(arr.as[CountryJson]))
}
}else {
None
}
}
}
)(NewUserJson.apply _)
}

Conditionally transforming a Json

I would like to keep field b only if field a is true.
{"a": true, "b": "value"} => {"a": true, "b": "value"}
{"a": false, "b": "value"} => {"a": false}
How can I do that with the Reads[JsObject]?
val blah: Reads[JsObject] = {
(__ \ 'a).json.pickBranch and
(__ \ 'b).json.pickBranch
}.reduce
I see a couple ways that you could do this without completely building the AST by hand. Depending on how many fields you want to pick or prune, one will be more concise than the other. Pulling the transform out to a variable would keep you from creating it every time.
val reads1: Reads[JsObject] = new Reads[JsObject] {
val prune = (__ \ 'b).json.prune
override def reads(json: JsValue): JsResult[JsObject] = {
(json \ "a").as[Boolean] match {
case true => json.validate[JsObject]
case false => json.transform(prune)
}
}
}
val reads2: Reads[JsObject] = new Reads[JsObject] {
val pick = (__ \ 'a).json.pickBranch
override def reads(json: JsValue): JsResult[JsObject] = {
(json \ "a").as[Boolean] match {
case true => json.validate[JsObject]
case false => json.transform(pick)
}
}
}

Play JSON: reading optional nested 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 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))),)
I try to have my formats match the json as closely as possible. Admittedly, in this case it's a bit awkward, but that's because the json schema is kind of weird. Here's how I would do it given those limitations:
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])
}
}
The output is:
[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)))),)
Note that the slightly awkward
(JsPath \ "change").read[List[Commit]].map(Changes.apply)
is necessary for single argument case classes.
EDIT:
The crucial part I missed is that parentNumber now becomes a method defined on Build as follows:
case class Build(buildType: BuildType, `artifact-dependencies`: Option[ArtifactDependencies], changes: Changes) {
def parentNumber: Option[String] = `artifact-dependencies`.flatMap(_.build.headOption.map(_.number))
}

scala play - edit JsValue with nested JsValues

the question is not specific for JsValue, its for all the immutable objects in scala which i want to edit part of it and keep the rest as is.
for example i have this object:
"references": {
"hsId": "37395615-244b-4706-b6f5-237272f07140",
"others": {
"path": "rewr",
"externalId": "ewr",
"version": "2"
}
}
and lets say i just want to edit the version.
thanks
ok i figured out a way of solution,
but i feel its a patch and not the best answer
val references: JsObject = (json \ "references").as[JsObject]
val newVersion = JsObject(List(("others", JsObject(List(("version", JsString("3")))).as[JsValue])))
val newReferences = references.deepMerge(newVersion)
You could use JSON transformers. Let's say we want to change the version to "3".
val js: JsValue = Json.parse("""
{
"references": {
"hsId": "37395615-244b-4706-b6f5-237272f07140",
"others": {
"path": "rewr",
"externalId": "ewr",
"version": "2"
}
}
}
""")
// Define the transformer
val transformer = (__ \ "references" \ "others").json.update(
__.read[JsObject].map{o => o ++ Json.obj("version" -> "3")}
)
val newJs = js.transform(transformer)
This will copy the entire object, then replace version on the others branch.

Play ScalaJSON Reads[T] parsing ValidationError(error.path.missing,WrappedArray())

i have a funny json data looking as:
[ {
"internal_network" : [ {
"address" : [ {
"address_id" : 2,
"address" : "172.16.20.1/24"
}, {
"address_id" : 1,
"address" : "172.16.30.30/24"
} ]
} ],
"switch_id" : "0000000000000001"
}, {
"internal_network" : [ {
"address" : [ {
"address_id" : 2,
"address" : "172.16.30.1/24"
}, {
"address_id" : 1,
"address" : "192.168.10.1/24"
}, {
"address_id" : 3,
"address" : "172.16.10.1/24"
} ]
} ],
"switch_id" : "0000000000000002"
} ]
i wrote case classes and custom reads:
case class TheAddress(addr: (Int, String))
implicit val theAddressReads: Reads[TheAddress] = (
(__ \ "address_id").read[Int] and
(__ \ "address").read[String] tupled) map (TheAddress.apply _)
case class Addresses(addr: List[TheAddress])
implicit val addressesReads: Reads[Addresses] =
(__ \ "address").read(list[TheAddress](theAddressReads)) map (Addresses.apply _)
case class TheSwitch(
switch_id: String,
address: List[Addresses] = Nil)
implicit val theSwitchReads: Reads[TheSwitch] = (
(__ \ "switch_id").read[String] and
(__ \ "internal_network").read(list[Addresses](addressesReads)))(TheSwitch)
case class Switches(col: List[TheSwitch])
implicit val switchesReads: Reads[Switches] =
(__ \ "").read(list[TheSwitch](theSwitchReads)) map (Switches.apply _)
when i validate the provided data with:
val json: JsValue = Json.parse(jsonChunk)
println(json.validate[TheSwitch])
i get:
JsError(List((/switch_id,List(ValidationError(error.path.missing,WrappedArray()))), (/internal_network,List(ValidationError(error.path.missing,WrappedArray())))))
i can access it with JsPath like
val switches: Seq[String] = (json \\ "switch_id").map(_.as[String])
but i'm really at my wits end with what am i doing wrong with custom reads.
i've tried with putting another top level key, and other combinations, but seems i'm missing something crucial, since i've started with this just today.
thanks a lot.
The error is telling you that instead of /switch_id it got an array. So it seems like you should read the JSON as a List[Switch] instead of just Switch
Assuming your Reads (didn't test them) are correct this should work:
val json: JsValue = Json.parse(jsonChunk)
println(json.validate[List[TheSwitch]])