Specify a nested object in scala's play json - json

Given this code:
case class SocialUser(firstName: String, lastName: String)
case class UserDetails(avatarUrl: String, phone: String)
// I want to avoid having to specify each SocialUser field one by one but just use the implicit write as stated below
implicit val socialUserWrites = Json.writes[SocialUser]
implicit val userDetailsWrites = Json.writes[UserDetails]
Now, how could I output the json in this format?
{"user": {
"firstName: "",
"lastName": "",
"details": {
"avatarUrl": "",
"phone": "",
}
}}

You miss "user" in "UserDetail" writes:
implicit val combinedUserWrites: Writes[CombinedUser] = (
(__ \ "user").write[SocialUser] and
(__ \ "user" \ "userDetails").write[UserDetails]
)(unlift(CombinedUser.unapply))
x: CombinedUser = CombinedUser(SocialUser(f,l),UserDetails(a,p))
scala> res4: play.api.libs.json.JsValue = {"user":{"firstName":"f","lastName":"l","userDetails":{"avatarUrl":"a","phone":"p"}}}

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

Convert json array to Seq of model class in Scala Play framework

I'm using Scala Play framework and Instagram API, and I want to extract a json array to my model class User:
case class User(val userId: String, val username: String, val profilePhoto: String, val name: String)
An json array example from the API is something like this:
{
"pagination": {},
"meta": {},
"data": [
{
"username": "carolinabentocb",
"profile_picture": "https://igcdn-photos-f-a.akamaihd.net/hphotos-ak-xfa1/t51.2885-19/s150x150/11429783_1673078532912085_1496721162_a.jpg",
"id": "363753337",
"full_name": "Carolina Bento"
},
{
"username": "pereira3044",
"profile_picture": "https://igcdn-photos-e-a.akamaihd.net/hphotos-ak-xaf1/t51.2885-19/s150x150/11351764_1662987433917180_971708049_a.jpg",
"id": "2141448590",
"full_name": "Alex"
}
]
}
In this link it is explained on how to map a json object to a model class, but how can I map the json array to a Seq/List/Array of Users?
The Json inception code is really great and it my preferred way to deserialize json. You will have to modify your User class to fit the instagram model API. Alternatively you could make a case class like InstagramApiUser or something to do the deserialization and copy to your own class later if you decide that is better for your flow. Here is the code and it works in a scala repl.
import play.api.libs.json.{Json, Format}
val js = Json.parse("""{
"pagination": {},
"meta": {},
"data": [
{
"username": "carolinabentocb",
"profile_picture": "https://igcdn-photos-f-a.akamaihd.net/hphotos-ak-xfa1/t51.2885-19/s150x150/11429783_1673078532912085_1496721162_a.jpg",
"id": "363753337",
"full_name": "Carolina Bento"
},
{
"username": "pereira3044",
"profile_picture": "https://igcdn-photos-e-a.akamaihd.net/hphotos-ak-xaf1/t51.2885-19/s150x150/11351764_1662987433917180_971708049_a.jpg",
"id": "2141448590",
"full_name": "Alex"
}
]
}""")
case class User(id: String, username: String, profile_picture: String, full_name: String)
object User {
implicit val jsonFormat: Format[User] = Json.format[User]
}
val result = (js \ "data").as[Seq[User]]
There are three methods to deserialize Json in the Play Json library, and as is the least idiomatic one in my opinion as it throws an exception if it fails to parse. You could try using asOpt[A] which will produce an Option[A] or better validate[A] which will produce a JsResult[A] and then you can log an error with the reason(s) that parsing your Json failed.
If you don't like naming your case class members to match the API names you can write the Reads manually like
import play.api.libs.json.{Json, Reads, JsPath}
import play.api.libs.functional.syntax._
case class User(val userId: String, val username: String, val profilePhoto: String, val name: String)
object User {
implicit val jsonReads: Reads[User] = (
(JsPath \ "id").read[String] and
(JsPath \ "username").read[String] and
(JsPath \ "profile_picture").read[String] and
(JsPath \ "full_name").read[String]
)(User.apply _)
}
And it works the same way otherwise.
The solution I've found is the following:
val users: Seq[User] = (json \ "data").as[JsArray].value.map(j => j.validate[User].get)
It may exist a more beautiful approach, but I will stick with this one until other answers.

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

Play Scala JSON Reads converter: mapping nested properties

I have the following case class:
case class User(name: String, age: String)
I am trying to implement a JSON Reads converter for it, so I can do the following:
val user = userJson.validate[User]
… but the incoming JSON has slightly different structure:
{ "age": "12", "details": { "name": "Bob" } }
How can I implement my JSON Reads converter?
You can do this using combinators to parse sub-paths.
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class User(name: String, age: String)
val js = Json.parse("""
{ "age": "12", "details": { "name": "Bob" } }
""")
implicit val reads: Reads[User] = (
(__ \ "details" \ "name").read[String] and
(__ \ "age").read[String]
)(User.apply _)
scala> js.validate[User]
res2: play.api.libs.json.JsResult[User] = JsSuccess(User(Bob,12),)

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