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

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

Related

Specify a nested object in scala's play 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"}}}

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

How to parse this JSON using Play Framework?

I have the following JSON being returned from a web service:
{
"hits": [
{
"created_at": "2016-02-01T15:01:03.000Z",
"title": "title",
"num_comments": 778,
"parent_id": null,
"_tags": [
"story",
"author",
"story_11012044"
],
"objectID": "11012044",
"_highlightResult": {
"title": {
"value": "title",
"matchLevel": "full",
"matchedWords": [
"title"
]
},
"author": {
"value": "author",
"matchLevel": "none",
"matchedWords": [
]
},
"story_text": {
"value": "Please lead",
"matchLevel": "none",
"matchedWords": [
]
}
}
}
]
}
and I am trying to parse it using the JSON parsing libs in Play Framework. I have the following code:
import play.api.libs.functional.syntax._
import play.api.libs.json._
case class Post(id: Long, date: String, count: Int)
object Post {
implicit val postFormat = Json.format[Post]
implicit val writes: Writes[Post] = (
(JsPath \ "id").write[Long] and
(JsPath \"date").write[String] and
(JsPath \ "count").write[Int]
)(unlift(Post.unapply))
implicit val reads: Reads[Post] = (
(JsPath \ "objectID").read[Long] and
(JsPath \ "created_at").read[String] and
(JsPath \ "num_comments").read[Int]
)(Post.apply _)
}
import play.api.libs.json._
class PostRepo {
val request: WSRequest = ws.url(MY_URL)
def getPosts: Future[Seq[Post]] =
val result: Future[JsValue] = request.get().map(response =>
response.status match {
case 200 => Json.parse(response.body)
case _ => throw new Exception("Web service call failed: " + response.body)
})
result.map( {
jsonvalue => println("JSARRAY: " + jsonvalue);
(jsonvalue \ "hits").as[Seq[Post]]
})
result
}
Now, when I run the code, I am getting the following error:
play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[JsResultException:
JsResultException(errors:List(((0)/date,List(ValidationError(List(error.path.missing),WrappedArray()))),
((0)/count,List(ValidationError(List(error.path.missing),WrappedArray()))),
((0)/id,List(ValidationError(List(error.path.missing),WrappedArray()))),
((1)/date,List(ValidationError(List(error.path.missing),WrappedArray()))),
((1)/count,List(ValidationError(List(error.path.missing),WrappedArray()))),
((1)/id,List(ValidationError(List(error.path.missing),WrappedArray()))),
((2)/date,List(ValidationError(List(error.path.missing),WrappedArray()))),
((2)/count,List(ValidationError(List(error.path.missing),WrappedArray()))),
((2)/id,List(ValidationError(List(error.path.missing),WrappedArray()))),
((3)/date,List(ValidationError(List(error.path.missing),WrappedArray()))),
((3)/count,List(ValidationError(List(error.path.missing),WrappedArray()))),
((3)/id,List(ValidationError(List(error.path.missing),WrappedArray())))
Obviously something is wrong with the way I'm trying to parse the JSON but I have now spent a few hours trying to figure out the problem and I'm well and truly stuck.
Some refactoring code with Reads.seq
val r = (__ \ "hits").read[Seq[Post]](Reads.seq[Post])
def getPosts: Future[Seq[Post]] = {
WS.url(MY_URL).get().map(response =>
response.status match {
case 200 => r.reads(response.json) match {
case JsError(e) => throw new Exception("Json read fails. Response body:" + response.json.toString() + "\nRead error:" + e.toString())
case JsSuccess(x, _) => x
}
case _ => throw new Exception("Web service call failed: " + response.body)
})
}

Read JSON field in depending on type specified by other field

I have JSON like the following:
{
"properties" : {
"timestamp" : "1970-01-01T01:00:00+01:00",
"attributes" : [
{
"name" : "Weather",
"value" : "Cloudy",
"fieldDataType" : "string"
},
{
"name" : "pH",
"value" : 7.2,
"fieldDataType" : "double"
},
{
"name" : "Quality Indicator",
"value" : 2,
"fieldDataType" : "integer"
}
]
}
and I want to parse it using the Play JSON libs. I've been able to handle the "timestamp" but am having difficulty in parsing the "value" field as its type is determined by "fieldDataType". So far I have:
sealed trait AttributeValue
case class AttributeInt(value: Integer) extends AttributeValue
case class AttributeDouble(value: Double) extends AttributeValue
case class AttributeString(value: String) extends AttributeValue
case class Attribute (name: String, value: AttributeValue)
object Attribute {
implicit val attributeReads: Reads[Attribute] = (
(JsPath \ "name").read[String] and
(JsPath \ "fieldDataType").read[String] // ???
)(Attribute.apply _)
}
I want to be able to read the "fieldDataType" and then, depending on its value, read in the "value" field. So if the "fieldDataType" is string then read the "value" as a string, if the "fieldDataType" is an "integer" then read the "value" as an integer etc.
First I did allow myself to change your AttributeInt declaration to:
case class AttributeInt(value: Int) extends AttributeValue
to use default Int parser
next you could define such Reads provider:
val attributeByDatatype: PartialFunction[String, JsPath => Reads[AttributeValue]] = {
case "integer" => _.read[Int].map(AttributeInt)
case "double" => _.read[Double].map(AttributeDouble)
case "string" => _.read[String].map(AttributeString)
}
being PartialFunction allow it not only to handle specific datatypes but also supply information about what datatype it knows and what it does not, this is useful with Reads.collect method, while resulting Reads could be target for flatMap
Now you could change your attributeReads as follows:
object Attribute {
implicit val attributeReads: Reads[Attribute] = (
(JsPath \ "name").read[String] and
(JsPath \ "fieldDataType")
.read[String]
.collect(ValidationError("datatype unknown"))(attributeByDatatype)
.flatMap(_(JsPath \ "value"))
)(Attribute.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)
}
}
}