I'm trying to parse json string with scala and playframework, I've read doc but still I'm stuck.
I got:
val jsonStr = """{
| "metric": "java.lang.Memory.HeapMemoryUsage.committed",
| "tags": {
| "instanceId": "ubuntu",
| "runId": "B_name_of_the_app-c4m8_0_2016-01-01_23-31-34"
| },
| "aggregateTags": [],
| "dps": {
| "1455711498": 8.71890944E8,
| "1455711558": 9.10688256E8,
| "1455711618": 9.24319744E8,
| "1455711678": 8.47773696E8,
| "1455711738": 9.35329792E8,
| "1455711798": 9.53679872E8,
| "1455714981": 1.983905792E9,
| "1455715041": 2.054684672E9,
| "1455715101": 2.05520896E9
| }
| }""".stripMargin
according to playframework doc I created classes to parse this thing:
// start of the scala file
import play.api.libs.json.{JsPath, Json, Reads}
import play.api.libs.functional.syntax._
case class Metric(metricName: String, tags: Tags, aggregateTags: Option[Seq[String]], dps: Seq[Map[String,Double]])
object Metric{
implicit val metricReads: Reads[Metric] = (
(JsPath \ "metric").read[String] and
(JsPath \ "tags").read[Tags] and
(JsPath \ "aggreagateTags").readNullable[Seq[String]] and
(JsPath \ "dps").read[Seq[Map[String,Double]]] //this one is tricky
)(Metric.apply _)
}
case class Tags(instanceId:String, runId: String)
object Tags{
implicit val tagsReads: Reads[Tags] = (
(JsPath \ "instanceId").read[String] and (JsPath \ "runId").read[String]
)(Tags.apply _)
}
Json.parse(jsonStr).validate[Metric]
// end of the scala file
Unfortunately validation results with:
res0: play.api.libs.json.JsResult[Metric] = JsError(List((//dps,List(ValidationError(List(error.expected.jsarray),WrappedArray())))))
I'm not sure how to solve this problem, also tried parse dps' as a separate class but also didn't work..Any tips?
You have (JsPath \ "dps").read[Seq[Map[String,Double]]] but dps in your JSON is not a Seq but a single entry - changing that part of the reader to (JsPath \ "dps").read[Map[String,Double]] will fix the problem.
Related
Say the JSON response I'm working with is formatted as follows:
[
{
"make": "Tesla",
"model": "Model S",
"year": 2017,
"color": "red",
"owner": "Bob",
"max_speed": 200,
"wheel_size": 30,
"is_convertible": true,
"license": "ABC123",
"cost": 50000,
"down_payment": 2500,
"other_property_1": 1,
"other_property_2": 2,
"other_property_3": 3,
"other_property_4": 4,
"other_property_5": 5,
"other_property_6": 6,
"other_property_7": 7,
"other_property_8": 8,
"other_property_9": 9,
"other_property_10": 10,
"other_property_11": 11
}
]
The JSON here is an array of car objects (just 1 for simplicity), and I am trying to convert this into a model using a JSON Reads converter. Let's say I have a Car case class to represent each object, and that class has has a nested FinancialInfo case class to split up the amount of attributes logically, so to avoid Scala's 22 parameter limit.
import play.api.libs.functional.syntax._
import play.api.libs.json._
case class Car(
make: String,
model: String,
year: Int,
color: String,
owner: String,
maxSpeed: Int,
wheelSize: Int,
isConvertible: Boolean,
license: String,
financialInfo: FinancialInfo, // nested case class to avoid 22 param limit
otherProperty1: Int,
otherProperty2: Int,
otherProperty3: Int,
otherProperty4: Int,
otherProperty5: Int,
otherProperty6: Int,
otherProperty7: Int,
otherProperty8: Int,
otherProperty9: Int,
otherProperty10: Int,
otherProperty11: Int
)
object Car {
implicit val reads: Reads[Car] = (
(__ \ "make").read[String] and
(__ \ "model").read[String] and
(__ \ "year").read[Int] and
(__ \ "color").read[String] and
(__ \ "owner").read[String] and
(__ \ "max_speed").read[Int] and
(__ \ "wheel_size").read[Int] and
(__ \ "is_convertible").read[Boolean] and
(__ \ "license").read[String] and
(__ \ "financialInfo").read[FinancialInfo] and
(__ \ "other_property_1").read[Int] and
(__ \ "other_property_2").read[Int] and
(__ \ "other_property_3").read[Int] and
(__ \ "other_property_4").read[Int] and
(__ \ "other_property_5").read[Int] and
(__ \ "other_property_6").read[Int] and
(__ \ "other_property_7").read[Int] and
(__ \ "other_property_8").read[Int] and
(__ \ "other_property_9").read[Int] and
(__ \ "other_property_10").read[Int] and
(__ \ "other_property_11").read[Int]
)(Car.apply _)
}
case class FinancialInfo(
cost: BigDecimal,
downPayment: BigDecimal
)
object FinancialInfo {
implicit val reads: Reads[FinancialInfo] = (
(__ \ "cost").read[BigDecimal] and
(__ \ "down_payment").read[BigDecimal]
)(FinancialInfo.apply _)
}
However, I'm guessing since there is no property in the JSON called financialInfo, it is not parsing it correctly. In my real application, I'm getting this error when I use response.json.validate[List[Car]]:
JsError(List(((0)/financialInfo,List(JsonValidationError(List(error.path.missing),WrappedArray())))))
To summarize, in the example, cost and down_payment are not contained in a nested object, even though for the Car case class I had to include a nested model called financialInfo. What is the best way to work around this error and make sure the values for cost and down_payment can be parsed? Any help or insight would be greatly appreciated!
Reads can be combined and included into each other.
So, having:
implicit val fiReads: Reads[FinancialInfo] = (
(JsPath \ "cost").read[BigDecimal] and
(JsPath \ "down_payment").read[BigDecimal]
)(FinancialInfo.apply _)
We can include it into the parent Reads:
implicit val carReads: Reads[Car] = (
(JsPath \ "make").read[String] and
(JsPath \ "model").read[String] and
fiReads // <--- HERE!
)(Car.apply _)
Now, with the following JSON:
private val json =
"""
|[
| {
| "make": "Tesla",
| "model": "Model S",
| "cost": 50000,
| "down_payment": 2500
| },
| {
| "make": "Tesla",
| "model": "Model D",
| "cost": 30000,
| "down_payment": 1500
| }
|]
""".stripMargin
val parsedJsValue = Json.parse(json)
val parsed = Json.fromJson[List[Car]](parsedJsValue)
println(parsed)
It is parsed properly:
JsSuccess(List(Car(Tesla,Model S,FinancialInfo(50000,2500)), Car(Tesla,Model D,FinancialInfo(30000,1500))),)
p.s. The Reads in the original question do no need to be wrapped into different objects. Related implicit values would be better inside same scope, closer to where they are actually used.
Is there a way to perform conditional logic while parsing json using Scala/Play?
For example, I would like to do something like the following:
implicit val playlistItemInfo: Reads[PlaylistItemInfo] = (
(if(( (JsPath \ "type1").readNullable[String]) != null){ (JsPath \ "type1" \ "id").read[String]} else {(JsPath \ "type2" \ "id").read[String]}) and
(JsPath \ "name").readNullable[String]
)(PlaylistItemInfo.apply _)
In my hypothetical JSON parsing example, there are two possible ways to parse the JSON. If the item is of "type1", then there will be a value for "type1" in the JSON. If this is not present in the JSON or its value is null/empty, then I would like to read the JSON node "type2" instead.
The above example does not work, but it gives you the idea of what I am trying to do.
Is this possible?
The proper way to do this with JSON combinators is to use orElse. Each piece of the combinator must be a Reads[YourType], so if/else doesn't quite work because your if clause doesn't return a Boolean, it returns Reads[PlaylistItemInfo] checked against null which will always be true. orElse let's us combine one Reads that looks for the type1 field, and a second one that looks for the type2 field as a fallback.
This might not follow your exact structure, but here's the idea:
import play.api.libs.json._
import play.api.libs.functional.syntax._
case class PlaylistItemInfo(id: Option[String], tpe: String)
object PlaylistItemInfo {
implicit val reads: Reads[PlaylistItemInfo] = (
(__ \ "id").readNullable[String] and
(__ \ "type1").read[String].orElse((__ \ "type2").read[String])
)(PlaylistItemInfo.apply _)
}
// Read type 1 over type 2
val js = Json.parse("""{"id": "test", "type1": "111", "type2": "2222"}""")
scala> js.validate[PlaylistItemInfo]
res1: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),111),)
// Read type 2 when type 1 is unavailable
val js = Json.parse("""{"id": "test", "type2": "22222"}""")
scala> js.validate[PlaylistItemInfo]
res2: play.api.libs.json.JsResult[PlaylistItemInfo] = JsSuccess(PlaylistItemInfo(Some(test),22222),)
// Error from neither
val js = Json.parse("""{"id": "test", "type100": "fake"}""")
scala> js.validate[PlaylistItemInfo]
res3: play.api.libs.json.JsResult[PlaylistItemInfo] = JsError(List((/type2,List(ValidationError(error.path.missing,WrappedArray())))))
I have the following Read defined:
import org.joda.time.DateTime;
implicit val userInfoRead: Reads[UserInfo] = (
(JsPath \ "userName").readNullable[String] and
] (JsPath \ "startDate").readNullable[DateTime]
(UserInfo.apply _)
With the following JSON object being passed in:
"userInfo" : {
"userName": "joeuser",
"startDate": "2006-02-28"
}
When I validate this data I get the following error:
(/startDate,List(ValidationError(validate.error.expected.jodadate.format,WrappedArray(yyyy-MM-dd))))))
Any suggestions on what I'm missing in the formatting?
As far as I can see, the issue is probably just the format not matching what Joda is expecting. I simplified a bit, and this worked for me:
scala> import org.joda.time.DateTime
import org.joda.time.DateTime
scala> case class UserInfo(userName: String, startDate: DateTime)
defined class UserInfo
scala> implicit val dateReads = Reads.jodaDateReads("yyyy-MM-dd")
dateReads: play.api.libs.json.Reads[org.joda.time.DateTime] = play.api.libs.json.DefaultReads$$anon$10#22db02cb
scala> implicit val userInfoReads = Json.reads[UserInfo]
userInfoReads: play.api.libs.json.Reads[UserInfo] = play.api.libs.json.Reads$$anon$8#52bcbd5d
scala> val json = Json.parse("""{
| "userName": "joeuser",
| "startDate": "2006-02-28"
| }""")
json: play.api.libs.json.JsValue = {"userName":"joeuser","startDate":"2006-02-28"}
scala> json.validate[UserInfo]
res12: play.api.libs.json.JsResult[UserInfo] = JsSuccess(UserInfo(joeuser,2006-02-28T00:00:00.000-05:00),)
Take into account the following JSON provided by a vendor API:
import play.api.libs.json._
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
val json = Json.parse(
"""
|{
| "returns": {
| "markets" : {
| "ABC" : {
| "label": "ABC",
| "id":1
| },
| "DEF" : {
| "label": "DEF",
| "id":2
| }
| }
| }
|}
""".stripMargin)
How to extract a sequence of pairs related to "label" and "id" fields.
From this piece of JSON the result I'm expecting is:
Seq((1,"ABC"),(2,"DEF"))
I'm failing with constructing a correct JsPath extractor because it expects a single match e.g.
val jsonTransformer = (__ \ 'returns \ 'markets).json.pick
json.transform(jsonTransformer)
Here's how I'd build this parser out of nice composable pieces. First for a general purpose object-to-array transformer that throws away keys:
val objToArray: Reads[JsArray] =
JsPath.json.pickBranch.map(obj => JsArray(obj.fields.map(_._2)))
Now for a Reads that can process market objects:
val marketReads: Reads[(Int, String)] =
((__ \ 'id).read[Int] and (__ \ 'label).read[String]).tupled
And now we tie it all together:
val pairsReads: Reads[List[(Int, String)]] =
(__ \ 'returns \ 'markets).read(objToArray) andThen list(marketReads)
And finally:
scala> json.validate(pairsReads).foreach(println)
List((1,ABC), (2,DEF))
Which is what you want. Note that I've specified the types above for clarity, but that's not necessary here—I'd probably leave them out in real code because the pieces are pretty small and straightforward.
This question already has an answer here:
Defining `Reads` for JSON Set Type
(1 answer)
Closed 8 years ago.
I have 3 case classes:
scala> case class Friend(id: Long, hobbies: List[Long])
defined class Friend
scala> case class Kid(friends: List[Friend])
defined class Kid
scala> case class Parent(kids: List[Kid])
defined class Parent
They can be shown through a hierarchy (Parent has Kid(s) has Friend(s) has (id and hobbies).
Parent
---> Kid
---> Friend
---> id: Long
---> hobbies: List[Long]
I created the FriendsReads (even though, as senia pointed out here, I could've just used
scala> implicit val FriendReads: Reads[Friend] = Json.format[Friend]
FriendReads: play.api.libs.json.Reads[Friend] = play.api.libs.json.OFormat$$anon$1#236b2692
scala> implicit val FriendReads: Reads[Friend] = (
| (JsPath \ "id").read[Long] and
| (JsPath \ "hobbies").read[List[Long]]
| )(Friend.apply _)
FriendReads: play.api.libs.json.Reads[Friend] = play.api.libs.json.Reads$$anon$8#4bb7bb4
Then, when I tried to create the KidReads, I ran into a compile-time problem.
scala> implicit val KidReads: Reads[Kid] = (
| (JsPath \ "friends").read[List[Friend]]
| )(Kid.apply _)
<console>:40: error: overloaded method value read with alternatives:
(t: List[Friend])play.api.libs.json.Reads[List[Friend]] <and>
(implicit r: play.api.libs.json.Reads[List[Friend]])play.api.libs.json.Reads[List[Friend]]
cannot be applied to (List[Friend] => Kid)
(JsPath \ "friends").read[List[Friend]]
^
How can I resolve this error?
Expression (JsPath \ "friends").read[List[Friend]] creates Reads[List[Friend]], you could convert it to Reads[Kid] using map method like this:
implicit val KidReads: Reads[Kid] =
(JsPath \ "friends").read[List[Friend]] map Kid.apply
Function Kid.apply will be applied to result of Reads[List[Friend]]#reads like this:
val friendListReads: Reads[List[Friend]] = (JsPath \ "friends").read[List[Friend]]
implicit val kidReads: Reads[Kid] = new Reads[Kid] {
def reads(json: JsValue): JsResult[Kid] = friendListReads.reads(json) match {
case JsSuccess(fList, path) = JsSuccess(Kid(fList), path)
case e: JsError => e
}
}